Skip to content

Commit 9bad607

Browse files
authored
Implement meshing for Capsule2d (#11639)
# Objective The `Capsule2d` primitive was added in #11585. It should support meshing like the other 2D primitives. ## Solution Implement meshing for `Capsule2d`. It doesn't currently support "rings" like Bevy's `Capsule` shape (not `Capsule3d`), but it does support resolution to control the number of vertices used for one hemicircle. The total vertex count is two times the resolution; if we allowed setting the full vertex count, odd numbers would lead to uneven vertex counts for the top and bottom hemicircles and produce potentially unwanted results. The capsule looks like this (with UV visualization and wireframe) using resolutions of 16, 8, and 3: ![Resolution 16](https://github.com/bevyengine/bevy/assets/57632562/feae22de-bdc5-438a-861f-848284b67a52) ![Resolution 8](https://github.com/bevyengine/bevy/assets/57632562/e95aab8e-793f-45ac-8a74-8be39f7626dd) ![Resolution of 3](https://github.com/bevyengine/bevy/assets/57632562/bcf01d23-1d8b-4cdb-966a-c9022a07c287) The `2d_shapes` example now includes the capsule, so we also get one more color of the rainbow 🌈 ![New 2D shapes example](https://github.com/bevyengine/bevy/assets/57632562/1c45b5f5-d26a-4e8c-8e8a-e106ab14d46e)
1 parent ab9447a commit 9bad607

File tree

3 files changed

+167
-9
lines changed

3 files changed

+167
-9
lines changed

crates/bevy_math/src/primitives/dim2.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,17 @@ pub struct Capsule2d {
769769
}
770770
impl Primitive2d for Capsule2d {}
771771

772+
impl Default for Capsule2d {
773+
/// Returns the default [`Capsule2d`] with a radius of `0.5` and a half-height of `0.5`,
774+
/// excluding the hemicircles.
775+
fn default() -> Self {
776+
Self {
777+
radius: 0.5,
778+
half_length: 0.5,
779+
}
780+
}
781+
}
782+
772783
impl Capsule2d {
773784
/// Create a new `Capsule2d` from a radius and length
774785
pub fn new(radius: f32, length: f32) -> Self {

crates/bevy_render/src/mesh/primitives/dim2.rs

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55

66
use super::Meshable;
77
use bevy_math::{
8-
primitives::{Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder},
8+
primitives::{Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder},
99
Vec2,
1010
};
1111
use wgpu::PrimitiveTopology;
@@ -266,3 +266,142 @@ impl From<Rectangle> for Mesh {
266266
rectangle.mesh()
267267
}
268268
}
269+
270+
/// A builder used for creating a [`Mesh`] with a [`Capsule2d`] shape.
271+
#[derive(Clone, Copy, Debug)]
272+
pub struct Capsule2dMeshBuilder {
273+
/// The [`Capsule2d`] shape.
274+
pub capsule: Capsule2d,
275+
/// The number of vertices used for one hemicircle.
276+
/// The total number of vertices for the capsule mesh will be two times the resolution.
277+
///
278+
/// The default is `16`.
279+
pub resolution: usize,
280+
}
281+
282+
impl Default for Capsule2dMeshBuilder {
283+
fn default() -> Self {
284+
Self {
285+
capsule: Capsule2d::default(),
286+
resolution: 16,
287+
}
288+
}
289+
}
290+
291+
impl Capsule2dMeshBuilder {
292+
/// Creates a new [`Capsule2dMeshBuilder`] from a given radius, length, and the number of vertices
293+
/// used for one hemicircle. The total number of vertices for the capsule mesh will be two times the resolution.
294+
#[inline]
295+
pub fn new(radius: f32, length: f32, resolution: usize) -> Self {
296+
Self {
297+
capsule: Capsule2d::new(radius, length),
298+
resolution,
299+
}
300+
}
301+
302+
/// Sets the number of vertices used for one hemicircle.
303+
/// The total number of vertices for the capsule mesh will be two times the resolution.
304+
#[inline]
305+
pub const fn resolution(mut self, resolution: usize) -> Self {
306+
self.resolution = resolution;
307+
self
308+
}
309+
310+
/// Builds a [`Mesh`] based on the configuration in `self`.
311+
pub fn build(&self) -> Mesh {
312+
// The resolution is the number of vertices for one semicircle
313+
let resolution = self.resolution as u32;
314+
let vertex_count = 2 * self.resolution;
315+
316+
// Six extra indices for the two triangles between the hemicircles
317+
let mut indices = Vec::with_capacity((self.resolution - 2) * 2 * 3 + 6);
318+
let mut positions = Vec::with_capacity(vertex_count);
319+
let normals = vec![[0.0, 0.0, 1.0]; vertex_count];
320+
let mut uvs = Vec::with_capacity(vertex_count);
321+
322+
let radius = self.capsule.radius;
323+
let step = std::f32::consts::TAU / vertex_count as f32;
324+
325+
// If the vertex count is even, offset starting angle of top semicircle by half a step
326+
// to position the vertices evenly.
327+
let start_angle = if vertex_count % 2 == 0 {
328+
step / 2.0
329+
} else {
330+
0.0
331+
};
332+
333+
// How much the hemicircle radius is of the total half-height of the capsule.
334+
// This is used to prevent the UVs from stretching between the hemicircles.
335+
let radius_frac = self.capsule.radius / (self.capsule.half_length + self.capsule.radius);
336+
337+
// Create top semicircle
338+
for i in 0..resolution {
339+
// Compute vertex position at angle theta
340+
let theta = start_angle + i as f32 * step;
341+
let (sin, cos) = theta.sin_cos();
342+
let (x, y) = (cos * radius, sin * radius + self.capsule.half_length);
343+
344+
positions.push([x, y, 0.0]);
345+
uvs.push([0.5 * (cos + 1.0), radius_frac * (1.0 - 0.5 * (sin + 1.0))]);
346+
}
347+
348+
// Add top semicircle indices
349+
for i in 1..resolution - 1 {
350+
indices.extend_from_slice(&[0, i, i + 1]);
351+
}
352+
353+
// Add indices for top left triangle of the part between the hemicircles
354+
indices.extend_from_slice(&[0, resolution - 1, resolution]);
355+
356+
// Create bottom semicircle
357+
for i in resolution..vertex_count as u32 {
358+
// Compute vertex position at angle theta
359+
let theta = start_angle + i as f32 * step;
360+
let (sin, cos) = theta.sin_cos();
361+
let (x, y) = (cos * radius, sin * radius - self.capsule.half_length);
362+
363+
positions.push([x, y, 0.0]);
364+
uvs.push([0.5 * (cos + 1.0), 1.0 - radius_frac * 0.5 * (sin + 1.0)]);
365+
}
366+
367+
// Add bottom semicircle indices
368+
for i in 1..resolution - 1 {
369+
indices.extend_from_slice(&[resolution, resolution + i, resolution + i + 1]);
370+
}
371+
372+
// Add indices for bottom right triangle of the part between the hemicircles
373+
indices.extend_from_slice(&[resolution, vertex_count as u32 - 1, 0]);
374+
375+
Mesh::new(
376+
PrimitiveTopology::TriangleList,
377+
RenderAssetUsages::default(),
378+
)
379+
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
380+
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
381+
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
382+
.with_indices(Some(Indices::U32(indices)))
383+
}
384+
}
385+
386+
impl Meshable for Capsule2d {
387+
type Output = Capsule2dMeshBuilder;
388+
389+
fn mesh(&self) -> Self::Output {
390+
Capsule2dMeshBuilder {
391+
capsule: *self,
392+
..Default::default()
393+
}
394+
}
395+
}
396+
397+
impl From<Capsule2d> for Mesh {
398+
fn from(capsule: Capsule2d) -> Self {
399+
capsule.mesh().build()
400+
}
401+
}
402+
403+
impl From<Capsule2dMeshBuilder> for Mesh {
404+
fn from(capsule: Capsule2dMeshBuilder) -> Self {
405+
capsule.build()
406+
}
407+
}

examples/2d/2d_shapes.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,39 @@ fn setup(
2020
commands.spawn(MaterialMesh2dBundle {
2121
mesh: meshes.add(Circle { radius: 50.0 }).into(),
2222
material: materials.add(Color::VIOLET),
23-
transform: Transform::from_translation(Vec3::new(-225.0, 0.0, 0.0)),
23+
transform: Transform::from_translation(Vec3::new(-275.0, 0.0, 0.0)),
2424
..default()
2525
});
2626

2727
// Ellipse
2828
commands.spawn(MaterialMesh2dBundle {
2929
mesh: meshes.add(Ellipse::new(25.0, 50.0)).into(),
3030
material: materials.add(Color::TURQUOISE),
31-
transform: Transform::from_translation(Vec3::new(-100.0, 0.0, 0.0)),
31+
transform: Transform::from_translation(Vec3::new(-150.0, 0.0, 0.0)),
32+
..default()
33+
});
34+
35+
// Capsule
36+
commands.spawn(MaterialMesh2dBundle {
37+
mesh: meshes.add(Capsule2d::new(25.0, 50.0)).into(),
38+
material: materials.add(Color::LIME_GREEN),
39+
transform: Transform::from_translation(Vec3::new(-50.0, 0.0, 0.0)),
3240
..default()
3341
});
3442

3543
// Rectangle
3644
commands.spawn(MaterialMesh2dBundle {
3745
mesh: meshes.add(Rectangle::new(50.0, 100.0)).into(),
38-
material: materials.add(Color::LIME_GREEN),
39-
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)),
46+
material: materials.add(Color::YELLOW),
47+
transform: Transform::from_translation(Vec3::new(50.0, 0.0, 0.0)),
4048
..default()
4149
});
4250

4351
// Hexagon
4452
commands.spawn(MaterialMesh2dBundle {
4553
mesh: meshes.add(RegularPolygon::new(50.0, 6)).into(),
46-
material: materials.add(Color::YELLOW),
47-
transform: Transform::from_translation(Vec3::new(125.0, 0.0, 0.0)),
54+
material: materials.add(Color::ORANGE),
55+
transform: Transform::from_translation(Vec3::new(175.0, 0.0, 0.0)),
4856
..default()
4957
});
5058

@@ -57,8 +65,8 @@ fn setup(
5765
Vec2::new(50.0, -50.0),
5866
))
5967
.into(),
60-
material: materials.add(Color::ORANGE),
61-
transform: Transform::from_translation(Vec3::new(250.0, 0.0, 0.0)),
68+
material: materials.add(Color::ORANGE_RED),
69+
transform: Transform::from_translation(Vec3::new(300.0, 0.0, 0.0)),
6270
..default()
6371
});
6472
}

0 commit comments

Comments
 (0)