Custom Arc Mesh in Bevy (Rust game engine)
How to create a custom arc Meshin the Bevy game engine. For some reason v 0.14.2 and below does not have one out-of-the-box.
Arc Mesh in Bevy (Rust game engine)
For whatever reason, Bevy (as of 0.14.2) does not have an out-of-the-box constructor for a Mesh that describes an arc.
Other simple shapes do exist eg:
let shapes = [
meshes.add(Circle::new(50.0)),
meshes.add(CircularSector::new(50.0, 1.0)),
meshes.add(CircularSegment::new(50.0, 1.25)),
meshes.add(Ellipse::new(25.0, 50.0)),
meshes.add(Annulus::new(25.0, 50.0)),
meshes.add(Capsule2d::new(25.0, 50.0)),
meshes.add(Rhombus::new(75.0, 100.0)),
meshes.add(Rectangle::new(50.0, 100.0)),
meshes.add(RegularPolygon::new(50.0, 6)),
meshes.add(Triangle2d::new(
Vec2::Y * 50.0,
Vec2::new(-50.0, -50.0),
Vec2::new(50.0, -50.0),
)),
];

Line of shapes described by the primitive pre-built meshes.
So lets build our own.
Disclaimer: I have killed too many brain cells from drinking coffee at Luke’s! I will no doubt have misused proper math / geometic terminology below. Diagrams should hopefully clearly indicate the indended meaning.
TL;DR
If you want an arc built like this:

Diagram showing the constituent parts of how we’re constructing the arc mesh.
Where:
- The centre of the mesh is the centre of the arc’s circle.
- Arc is constructed starting from 12 o’clock and spans counter-clockwise a given angle in radians.
Use this:
fn build_arc_mesh(
arc_angle: f32,
outer_radius: f32,
inner_radius: f32,
segment_resolution: u32,
) -> Mesh {
let mut arc_mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
let mut v_pos: Vec<[f32; 3]> = vec![];
let mut indices: Vec<u32> = vec![];
let segment_angle = arc_angle / (segment_resolution as f32);
for i in 0..segment_resolution {
let angle = i as f32 * segment_angle;
let next_angle = (i + 1) as f32 * segment_angle;
let rotation_vector = Vec2::from_angle(angle);
let next_rotation_vector = Vec2::from_angle(next_angle);
let outer_point = (Vec2::Y * outer_radius).rotate(rotation_vector).extend(0.0);
let next_outer_point = (Vec2::Y * outer_radius).rotate(next_rotation_vector).extend(0.0);
let inner_point = (Vec2::Y * inner_radius).rotate(rotation_vector).extend(0.0);
let next_inner_point = (Vec2::Y * inner_radius).rotate(next_rotation_vector).extend(0.0);
let base_index = v_pos.len() as u32;
indices.push(base_index);
indices.push(base_index + 1);
indices.push(base_index + 2);
indices.push(base_index + 1);
indices.push(base_index + 2);
indices.push(base_index + 3);
v_pos.push([outer_point.x, outer_point.y, outer_point.z]);
v_pos.push([inner_point.x, inner_point.y, inner_point.z]);
v_pos.push([next_outer_point.x, next_outer_point.y, next_outer_point.z]);
v_pos.push([next_inner_point.x, next_inner_point.y, next_inner_point.z]);
}
arc_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, v_pos);
arc_mesh.insert_indices(Indices::U32(indices));
arc_mesh
}
Boilerplate
First lets get all the usual boilerplate in to run a window with a 2D camera.
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, (spawn_2d_camera, spawn_arc_mesh))
.run();
}
fn spawn_2d_camera(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}
fn fn spawn_arc_mesh(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
){
}
Arc Mesh Builder
Lets right out of the gate abstract out a method to return us a Mesh that we can use wherever we like.
nb: all angles are in radians and all rotations are counter-clockwise. Just remember that PI is exactly half a circle in radians and we can measure out from there.
fn build_arc_mesh(
arc_angle: f32,
outer_radius: f32,
inner_radius: f32,
segment_resolution: u32,
) -> Mesh {
}

Diagram showing the angle, inner radius, and outer radius of an arc.
Some info on what the inputs mean for us non maths-brained people (inner and outer radius are self-explanatory):
arc_angle: f32this is the angle of the circle that the arc will take up. For example if we usedPIas an input, our arc would be half a circle.segment_resolution: u32Our mesh is actually a collection of triangles which obviously have straight sides. Consequently the more of them we have the smoother thecurve will have. For instance an arc with an angle ofPI * 2.0(a full circle) and a segment resolution of4will simply give us a square!
Now lets add some functionality to build_arc_mesh
fn build_arc_mesh(
arc_angle: f32,
outer_radius: f32,
inner_radius: f32,
segment_resolution: u32,
) -> Mesh {
let mut arc_mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
arc_mesh
}
Here Mesh::new is giving us a new mesh object where:
PrimitiveTopology::TriangleListis telling the mesh that our shape is a collection of triangles.RenderAssetUsages::RENDER_WORLDdefines where it will be used. Notably this setting means this mesh will NOT be available to retrieve as a resource from theasset_serverafter this first frame.
fn build_arc_mesh(
arc_angle: f32,
outer_radius: f32,
inner_radius: f32,
segment_resolution: u32,
) -> Mesh {
let mut arc_mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
let mut v_pos: Vec<[f32; 3]> = vec![];
let mut indices: Vec<u32> = vec![];
arc_mesh
}
v_pos is our array of vertices (points) that make up our triangles. All meshes are defined in 3d space but we’re working in 2d here so the third (z) value of every vertex will be 0.0.
indicies defines the ‘order’ of points when building our shape. Note that touching triangles will share points so the values in this can refer to the same vertex multiple times.

Diagram showing how four points makes two triangles where the long side of the triangles share a pair of verticies.
For example these four points describe two triangles, so our v_pos list might look like: (30,15), (30,0), (0,15), (0,0)
And our indicies list might look like: 1, 2, 3, 2, 3, 4 where the long edge of both triangles share the same verticies.
fn build_arc_mesh(
arc_angle: f32,
outer_radius: f32,
inner_radius: f32,
segment_resolution: u32,
) -> Mesh {
let mut arc_mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
let mut v_pos: Vec<[f32; 3]> = vec![];
let mut indices: Vec<u32> = vec![];
let segment_angle = arc_angle / (segment_resolution as f32);
for i in 0..segment_resolution {
let angle = i as f32 * segment_angle;
let next_angle = (i + 1) as f32 * segment_angle;
let rotation_vector = Vec2::from_angle(angle);
let next_rotation_vector = Vec2::from_angle(next_angle);
let outer_point = (Vec2::Y * outer_radius)
.rotate(rotation_vector)
.extend(0.0);
let next_outer_point = (Vec2::Y * outer_radius)
.rotate(next_rotation_vector)
.extend(0.0);
let inner_point = (Vec2::Y * inner_radius)
.rotate(rotation_vector)
.extend(0.0);
let next_inner_point = (Vec2::Y * inner_radius)
.rotate(next_rotation_vector)
.extend(0.0);
}
arc_mesh
}
From our known amount of segments and angle, we can calculate the segment_angle for each chunk of our arc.

Diagram showing what the segment_angle covers in relation to the arc_angle.
For each segment we need two angles to describe the chunk of our arc: the start and end position.
For both the start and the end we can get the inner point and the outer point by two a Vec::Y (ie, UP). Extending it to reach the distances defined by the inner and outer radii, then rotating them by the angle and next angle.

Diagram showing where the points sit in the arc segment.
We now collect these points and add their ordering to the indicies array, before adding them both as attributes of the mesh.
fn build_arc_mesh(
arc_angle: f32,
outer_radius: f32,
inner_radius: f32,
segment_resolution: u32,
) -> Mesh {
let mut arc_mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
let mut v_pos: Vec<[f32; 3]> = vec![];
let mut indices: Vec<u32> = vec![];
let segment_angle = arc_angle / (segment_resolution as f32);
for i in 0..segment_resolution {
let angle = i as f32 * segment_angle;
let next_angle = (i + 1) as f32 * segment_angle;
let rotation_vector = Vec2::from_angle(angle);
let next_rotation_vector = Vec2::from_angle(next_angle);
let outer_point = (Vec2::Y * outer_radius)
.rotate(rotation_vector)
.extend(0.0);
let next_outer_point = (Vec2::Y * outer_radius)
.rotate(next_rotation_vector)
.extend(0.0);
let inner_point = (Vec2::Y * inner_radius)
.rotate(rotation_vector)
.extend(0.0);
let next_inner_point = (Vec2::Y * inner_radius)
.rotate(next_rotation_vector)
.extend(0.0);
let base_index = v_pos.len() as
indices.push(base_index);
indices.push(base_index + 1);
indices.push(base_index + 2);
indices.push(base_index + 1);
indices.push(base_index + 2);
indices.push(base_index + 3);
// add the points to the list of vertexes
v_pos.push([outer_point.x, outer_point.y, outer_point.z]);
v_pos.push([inner_point.x, inner_point.y, inner_point.z]);
v_pos.push([next_outer_point.x, next_outer_point.y, next_outer_point.z]);
v_pos.push([next_inner_point.x, next_inner_point.y, next_inner_point.z]);
}
arc_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, v_pos);
arc_mesh.insert_indices(Indices::U32(indices));
arc_mesh
}
Note the repeat of adding base_index+1 and base_index+2 this is the shared side of the pair of triangles:

Diagram highlighting the order of the vertexes that compose two triangles in an arcs segment.
Using our new Arc
With our arc builder in place we can update our spawn method like so:
fn spawn_arc_mesh(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let mesh = build_arc_mesh(
PI, // PI is half a circle in radians
100.0, // outer radius
90.0, // inner radius (ie, this arc will be 10 units thick)
10, // the number of chunks on the curve (more looks smoother)
);
let mesh_handle = Mesh2dHandle(meshes.add(mesh));
let color_material = materials.add(Color::WHITE);
commands.spawn(MaterialMesh2dBundle {
mesh: mesh_handle,
material: color_material,
..Default::default()
});
}
And voilà:

Bevy application window showing an arc spanning counter-clockwise from the 12 oclock position to the 6 oclock position.
Note that since we constructed our arc by measuring outwards from the origin, the middle of the mesh is where the centre of the circle would be (i.e. if you rotate this mesh it will ‘spin’ around like a loading spinner)
Exercise for reader
- Add checking that the inner and outer radius are valid.
- Add more triangles to create smooth caps to the ends of the arc.
- Update the builder such that the mesh can be accessed from the
asset_serverafter the first frame (for example if you need to change the size of the mesh) - Reduce the number of verts added per loop. This implementation is adding the first flat edge of verts for each iteration, but they’re already there from the last segment (not including the first segment)
- Reduce number of triangles per arc. This implementation is adding more verticies than necessary (see diagram below)

Diagram of arc construction with fewer triangles
메타데이터
- post_id
- fb7fca7e81fd
- slug
- custom-arc-mesh-in-bevy-rust-game-engine-fb7fca7e81fd
- url
- https://medium.com/@CyberRory-3000/custom-arc-mesh-in-bevy-rust-game-engine-fb7fca7e81fd
- canonical_url
- https://medium.com/@CyberRory-3000/custom-arc-mesh-in-bevy-rust-game-engine-fb7fca7e81fd
- author_url
- https://medium.com/@CyberRory-3000
- status
- ok
- fetched_at
- 2026-06-15 20:49:13