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.

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: