Skip to content

Implement meshing for Capsule2d #11639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,17 @@ pub struct Capsule2d {
}
impl Primitive2d for Capsule2d {}

impl Default for Capsule2d {
/// Returns the default [`Capsule2d`] with a radius of `0.5` and a half-height of `0.5`,
/// excluding the hemicircles.
fn default() -> Self {
Self {
radius: 0.5,
half_length: 0.5,
}
}
}

impl Capsule2d {
/// Create a new `Capsule2d` from a radius and length
pub fn new(radius: f32, length: f32) -> Self {
Expand Down
141 changes: 140 additions & 1 deletion crates/bevy_render/src/mesh/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{

use super::Meshable;
use bevy_math::{
primitives::{Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder},
primitives::{Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder},
Vec2,
};
use wgpu::PrimitiveTopology;
Expand Down Expand Up @@ -266,3 +266,142 @@ impl From<Rectangle> for Mesh {
rectangle.mesh()
}
}

/// A builder used for creating a [`Mesh`] with a [`Capsule2d`] shape.
#[derive(Clone, Copy, Debug)]
pub struct Capsule2dMeshBuilder {
/// The [`Capsule2d`] shape.
pub capsule: Capsule2d,
/// The number of vertices used for one hemicircle.
/// The total number of vertices for the capsule mesh will be two times the resolution.
///
/// The default is `16`.
pub resolution: usize,
}

impl Default for Capsule2dMeshBuilder {
fn default() -> Self {
Self {
capsule: Capsule2d::default(),
resolution: 16,
}
}
}

impl Capsule2dMeshBuilder {
/// Creates a new [`Capsule2dMeshBuilder`] from a given radius, length, and the number of vertices
/// used for one hemicircle. The total number of vertices for the capsule mesh will be two times the resolution.
#[inline]
pub fn new(radius: f32, length: f32, resolution: usize) -> Self {
Self {
capsule: Capsule2d::new(radius, length),
resolution,
}
}

/// Sets the number of vertices used for one hemicircle.
/// The total number of vertices for the capsule mesh will be two times the resolution.
#[inline]
pub const fn resolution(mut self, resolution: usize) -> Self {
self.resolution = resolution;
self
}

/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
// The resolution is the number of vertices for one semicircle
let resolution = self.resolution as u32;
let vertex_count = 2 * self.resolution;

// Six extra indices for the two triangles between the hemicircles
let mut indices = Vec::with_capacity((self.resolution - 2) * 2 * 3 + 6);
let mut positions = Vec::with_capacity(vertex_count);
let normals = vec![[0.0, 0.0, 1.0]; vertex_count];
let mut uvs = Vec::with_capacity(vertex_count);

let radius = self.capsule.radius;
let step = std::f32::consts::TAU / vertex_count as f32;

// If the vertex count is even, offset starting angle of top semicircle by half a step
// to position the vertices evenly.
let start_angle = if vertex_count % 2 == 0 {
step / 2.0
} else {
0.0
};

// How much the hemicircle radius is of the total half-height of the capsule.
// This is used to prevent the UVs from stretching between the hemicircles.
let radius_frac = self.capsule.radius / (self.capsule.half_length + self.capsule.radius);

// Create top semicircle
for i in 0..resolution {
// Compute vertex position at angle theta
let theta = start_angle + i as f32 * step;
let (sin, cos) = theta.sin_cos();
let (x, y) = (cos * radius, sin * radius + self.capsule.half_length);

positions.push([x, y, 0.0]);
uvs.push([0.5 * (cos + 1.0), radius_frac * (1.0 - 0.5 * (sin + 1.0))]);
}

// Add top semicircle indices
for i in 1..resolution - 1 {
indices.extend_from_slice(&[0, i, i + 1]);
}

// Add indices for top left triangle of the part between the hemicircles
indices.extend_from_slice(&[0, resolution - 1, resolution]);

// Create bottom semicircle
for i in resolution..vertex_count as u32 {
// Compute vertex position at angle theta
let theta = start_angle + i as f32 * step;
let (sin, cos) = theta.sin_cos();
let (x, y) = (cos * radius, sin * radius - self.capsule.half_length);

positions.push([x, y, 0.0]);
uvs.push([0.5 * (cos + 1.0), 1.0 - radius_frac * 0.5 * (sin + 1.0)]);
}

// Add bottom semicircle indices
for i in 1..resolution - 1 {
indices.extend_from_slice(&[resolution, resolution + i, resolution + i + 1]);
}

// Add indices for bottom right triangle of the part between the hemicircles
indices.extend_from_slice(&[resolution, vertex_count as u32 - 1, 0]);

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

impl Meshable for Capsule2d {
type Output = Capsule2dMeshBuilder;

fn mesh(&self) -> Self::Output {
Capsule2dMeshBuilder {
capsule: *self,
..Default::default()
}
}
}

impl From<Capsule2d> for Mesh {
fn from(capsule: Capsule2d) -> Self {
capsule.mesh().build()
}
}

impl From<Capsule2dMeshBuilder> for Mesh {
fn from(capsule: Capsule2dMeshBuilder) -> Self {
capsule.build()
}
}
24 changes: 16 additions & 8 deletions examples/2d/2d_shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,39 @@ fn setup(
commands.spawn(MaterialMesh2dBundle {
mesh: meshes.add(Circle { radius: 50.0 }).into(),
material: materials.add(Color::VIOLET),
transform: Transform::from_translation(Vec3::new(-225.0, 0.0, 0.0)),
transform: Transform::from_translation(Vec3::new(-275.0, 0.0, 0.0)),
..default()
});

// Ellipse
commands.spawn(MaterialMesh2dBundle {
mesh: meshes.add(Ellipse::new(25.0, 50.0)).into(),
material: materials.add(Color::TURQUOISE),
transform: Transform::from_translation(Vec3::new(-100.0, 0.0, 0.0)),
transform: Transform::from_translation(Vec3::new(-150.0, 0.0, 0.0)),
..default()
});

// Capsule
commands.spawn(MaterialMesh2dBundle {
mesh: meshes.add(Capsule2d::new(25.0, 50.0)).into(),
material: materials.add(Color::LIME_GREEN),
transform: Transform::from_translation(Vec3::new(-50.0, 0.0, 0.0)),
..default()
});

// Rectangle
commands.spawn(MaterialMesh2dBundle {
mesh: meshes.add(Rectangle::new(50.0, 100.0)).into(),
material: materials.add(Color::LIME_GREEN),
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)),
material: materials.add(Color::YELLOW),
transform: Transform::from_translation(Vec3::new(50.0, 0.0, 0.0)),
..default()
});

// Hexagon
commands.spawn(MaterialMesh2dBundle {
mesh: meshes.add(RegularPolygon::new(50.0, 6)).into(),
material: materials.add(Color::YELLOW),
transform: Transform::from_translation(Vec3::new(125.0, 0.0, 0.0)),
material: materials.add(Color::ORANGE),
transform: Transform::from_translation(Vec3::new(175.0, 0.0, 0.0)),
..default()
});

Expand All @@ -57,8 +65,8 @@ fn setup(
Vec2::new(50.0, -50.0),
))
.into(),
material: materials.add(Color::ORANGE),
transform: Transform::from_translation(Vec3::new(250.0, 0.0, 0.0)),
material: materials.add(Color::ORANGE_RED),
transform: Transform::from_translation(Vec3::new(300.0, 0.0, 0.0)),
..default()
});
}