Drawing Lines in 2D

2023-10-18

Bevy 0.11 added an immediate mode gizmos API that can draw lines.

The immediate mode API is useful for quick debugging, but it has some limitations:

  • Gizmos are always drawn on top of other content
  • Gizmos aren't retained between frames. You must write code that draws them every frame.

If you want to just "spawn a line segment entity," you can use a sprite! A line segment is just a very skinny rectangle, after all.

Bevy 0.15
fn line_segment(
    start: Vec2,
    end: Vec2,
    thickness: f32,
    color: Color,
) -> impl Bundle {
    let length = start.distance(end);
    let diff = start - end;
    let theta = diff.y.atan2(diff.x);
    let midpoint = (start + end) / 2.;

    let transform = Transform::from_xyz(midpoint.x, midpoint.y, 0.)
        .with_rotation(Quat::from_rotation_z(theta));

    (
        Sprite {
            color,
            custom_size: Some(Vec2::new(length, thickness)),
            ..default()
        },
        transform,
    )
}

fn setup(mut commands: Commands) {
    commands.spawn(line_segment(
        Vec2::new(0.0, 0.0),
        Vec2::new(100.0, 200.0),
        3.0,
        Color::WHITE,
    ));
}

Another way to do this would be to create a custom Mesh. A really handy plugin that takes this approach is bevy_prototype_lyon. It does tesselation with lyon and supports polylines with nice joinery and other arbitrary 2d shapes.

Note

Creating a unique mesh for each line segment you want to draw will be less performant than using sprites, because Bevy won't be able to use automatic batching.

Here's what DIYing it for a simple line segment looks like:

Bevy 0.15
pub struct LineSegment {
    pub start: Vec2,
    pub end: Vec2,
    pub thickness: f32,
}

impl From<LineSegment> for Mesh {
    fn from(segment: LineSegment) -> Self {
        let LineSegment {
            start,
            end,
            thickness,
        } = segment;

        let dir = (start - end).normalize();
        let perp = Vec2::new(-dir.y, dir.x);

        let half_thickness = thickness / 2.;

        let s1 = start + perp * -half_thickness;
        let s2 = start + perp * half_thickness;
        let e1 = end + perp * -half_thickness;
        let e2 = end + perp * half_thickness;

        let vertices = [
            ([s1.x, s1.y, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0]),
            ([s2.x, s2.y, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0]),
            ([e1.x, e1.y, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0]),
            ([e2.x, e2.y, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0]),
        ];

        let indices = Indices::U32(vec![0, 1, 2, 1, 3, 2]);

        let positions: Vec<_> = vertices.iter().map(|(p, _, _)| *p).collect();
        let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect();
        let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect();

        Mesh::new(
            PrimitiveTopology::TriangleList,
            RenderAssetUsages::default(),
        )
        .with_inserted_indices(indices)
        .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
        .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
        .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
    }
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn((
        Mesh2d(meshes.add(Mesh::from(LineSegment {
            start: Vec2::new(0., 0.),
            end: Vec2::new(100., 200.),
            thickness: 3.0,
        }))),
        MeshMaterial2d(materials.add(ColorMaterial::from(Color::WHITE))),
    ));
}

If you need more power, check out these options from the Bevy's third party ecosystem: