diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index ba105e8780bd46..2befff68089410 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -769,6 +769,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 { diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 7f6bbfb1a8b004..9578dee5a27deb 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -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; @@ -266,3 +266,142 @@ impl From 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 for Mesh { + fn from(capsule: Capsule2d) -> Self { + capsule.mesh().build() + } +} + +impl From for Mesh { + fn from(capsule: Capsule2dMeshBuilder) -> Self { + capsule.build() + } +} diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 6ac3736a53851d..7676713c4499dd 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -20,7 +20,7 @@ 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() }); @@ -28,23 +28,31 @@ fn setup( 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() }); @@ -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() }); }