From f8e18e07920f2ffa1fcf2506e1e1d32bd6ef1ba0 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 3 Feb 2024 20:03:43 +0200 Subject: [PATCH] Implement meshing for `Capsule2d` (#11639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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) --- crates/bevy_math/src/primitives/dim2.rs | 11 ++ .../bevy_render/src/mesh/primitives/dim2.rs | 141 +++++++++++++++++- examples/2d/2d_shapes.rs | 24 ++- 3 files changed, 167 insertions(+), 9 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index ba105e8780bd4..2befff6808941 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 7f6bbfb1a8b00..9578dee5a27de 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 6ac3736a53851..7676713c4499d 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() }); }