From ff560e301168ca190afb487781bc34dbdff2af8a Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Thu, 5 May 2022 00:03:47 +0000 Subject: [PATCH] Add RegularPolygon and Circle meshes (#3730) # Objective Bevy users often want to create circles and other simple shapes. All the machinery is in place to accomplish this, and there are external crates that help. But when writing code for e.g. a new bevy example, it's not really possible to draw a circle without bringing in a new asset, writing a bunch of scary looking mesh code, or adding a dependency. In particular, this PR was inspired by this interaction in another PR: https://github.com/bevyengine/bevy/pull/3721#issuecomment-1016774535 ## Solution This PR adds `shape::RegularPolygon` and `shape::Circle` (which is just a `RegularPolygon` that defaults to a large number of sides) ## Discussion There's a lot of ongoing discussion about shapes in and at least one other lingering shape PR (although it seems incomplete). That RFC currently includes `RegularPolygon` and `Circle` shapes, so I don't think that having working mesh generation code in the engine for those shapes would add much burden to an author of an implementation. But if we'd prefer not to add additional shapes until after that's sorted out, I'm happy to close this for now. ## Alternatives for users For any users stumbling on this issue, here are some plugins that will help if you need more shapes. https://github.com/Nilirad/bevy_prototype_lyon https://github.com/johanhelsing/bevy_smud https://github.com/Weasy666/bevy_svg https://github.com/redpandamonium/bevy_more_shapes https://github.com/ForesightMiningSoftwareCorporation/bevy_polyline --- Cargo.toml | 4 +- crates/bevy_render/src/mesh/shape/mod.rs | 2 + .../src/mesh/shape/regular_polygon.rs | 103 ++++++++++++++++++ examples/2d/rect.rs | 20 ---- examples/2d/shapes.rs | 42 +++++++ examples/README.md | 2 +- 6 files changed, 150 insertions(+), 23 deletions(-) create mode 100644 crates/bevy_render/src/mesh/shape/regular_polygon.rs delete mode 100644 examples/2d/rect.rs create mode 100644 examples/2d/shapes.rs diff --git a/Cargo.toml b/Cargo.toml index 1e3040f0bda6da..60b424bf56e293 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,8 +144,8 @@ name = "mesh2d_manual" path = "examples/2d/mesh2d_manual.rs" [[example]] -name = "rect" -path = "examples/2d/rect.rs" +name = "shapes" +path = "examples/2d/shapes.rs" [[example]] name = "sprite" diff --git a/crates/bevy_render/src/mesh/shape/mod.rs b/crates/bevy_render/src/mesh/shape/mod.rs index 31f9f8b5d4bd25..00ce6f1eefc432 100644 --- a/crates/bevy_render/src/mesh/shape/mod.rs +++ b/crates/bevy_render/src/mesh/shape/mod.rs @@ -224,11 +224,13 @@ impl From for Mesh { mod capsule; mod icosphere; +mod regular_polygon; mod torus; mod uvsphere; pub use capsule::{Capsule, CapsuleUvProfile}; pub use icosphere::Icosphere; +pub use regular_polygon::{Circle, RegularPolygon}; pub use torus::Torus; pub use uvsphere::UVSphere; use wgpu::PrimitiveTopology; diff --git a/crates/bevy_render/src/mesh/shape/regular_polygon.rs b/crates/bevy_render/src/mesh/shape/regular_polygon.rs new file mode 100644 index 00000000000000..ffb3e9d08e1d76 --- /dev/null +++ b/crates/bevy_render/src/mesh/shape/regular_polygon.rs @@ -0,0 +1,103 @@ +use crate::mesh::{Indices, Mesh}; +use wgpu::PrimitiveTopology; + +/// A regular polygon in the xy plane +#[derive(Debug, Copy, Clone)] +pub struct RegularPolygon { + /// Inscribed radius in the xy plane. + pub radius: f32, + /// Number of sides. + pub sides: usize, +} + +impl Default for RegularPolygon { + fn default() -> Self { + Self { + radius: 0.5, + sides: 6, + } + } +} + +impl RegularPolygon { + /// Creates a regular polygon in the xy plane + pub fn new(radius: f32, sides: usize) -> Self { + Self { radius, sides } + } +} + +impl From for Mesh { + fn from(polygon: RegularPolygon) -> Self { + let RegularPolygon { radius, sides } = polygon; + + debug_assert!(sides > 2, "RegularPolygon requires at least 3 sides."); + + let mut positions = Vec::with_capacity(sides); + let mut normals = Vec::with_capacity(sides); + let mut uvs = Vec::with_capacity(sides); + + let step = std::f32::consts::TAU / sides as f32; + for i in 0..sides { + let theta = std::f32::consts::FRAC_PI_2 - i as f32 * step; + let (sin, cos) = theta.sin_cos(); + + positions.push([cos * radius, sin * radius, 0.0]); + normals.push([0.0, 0.0, 1.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + let mut indices = Vec::with_capacity((sides - 2) * 3); + for i in 1..(sides as u32 - 1) { + indices.extend_from_slice(&[0, i + 1, i]); + } + + let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); + mesh.set_indices(Some(Indices::U32(indices))); + mesh + } +} + +/// A circle in the xy plane +pub struct Circle { + /// Inscribed radius in the xy plane. + pub radius: f32, + /// The number of vertices used. + pub vertices: usize, +} + +impl Default for Circle { + fn default() -> Self { + Self { + radius: 0.5, + vertices: 64, + } + } +} + +impl Circle { + /// Creates a circle in the xy plane + pub fn new(radius: f32) -> Self { + Self { + radius, + ..Default::default() + } + } +} + +impl From for RegularPolygon { + fn from(circle: Circle) -> Self { + Self { + radius: circle.radius, + sides: circle.vertices, + } + } +} + +impl From for Mesh { + fn from(circle: Circle) -> Self { + Mesh::from(RegularPolygon::from(circle)) + } +} diff --git a/examples/2d/rect.rs b/examples/2d/rect.rs deleted file mode 100644 index c87cced47bd5e7..00000000000000 --- a/examples/2d/rect.rs +++ /dev/null @@ -1,20 +0,0 @@ -use bevy::prelude::*; - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_startup_system(setup) - .run(); -} - -fn setup(mut commands: Commands) { - commands.spawn_bundle(OrthographicCameraBundle::new_2d()); - commands.spawn_bundle(SpriteBundle { - sprite: Sprite { - color: Color::rgb(0.25, 0.25, 0.75), - custom_size: Some(Vec2::new(50.0, 50.0)), - ..default() - }, - ..default() - }); -} diff --git a/examples/2d/shapes.rs b/examples/2d/shapes.rs new file mode 100644 index 00000000000000..5fe8653ca68a69 --- /dev/null +++ b/examples/2d/shapes.rs @@ -0,0 +1,42 @@ +use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + + // Rectangle + commands.spawn_bundle(SpriteBundle { + sprite: Sprite { + color: Color::rgb(0.25, 0.25, 0.75), + custom_size: Some(Vec2::new(50.0, 100.0)), + ..default() + }, + ..default() + }); + + // Circle + commands.spawn_bundle(MaterialMesh2dBundle { + mesh: meshes.add(shape::Circle::new(50.).into()).into(), + material: materials.add(ColorMaterial::from(Color::PURPLE)), + transform: Transform::from_translation(Vec3::new(-100., 0., 0.)), + ..default() + }); + + // Hexagon + commands.spawn_bundle(MaterialMesh2dBundle { + mesh: meshes.add(shape::RegularPolygon::new(50., 6).into()).into(), + material: materials.add(ColorMaterial::from(Color::TURQUOISE)), + transform: Transform::from_translation(Vec3::new(100., 0., 0.)), + ..default() + }); +} diff --git a/examples/README.md b/examples/README.md index 4d4103e7298951..041d048f353a27 100644 --- a/examples/README.md +++ b/examples/README.md @@ -89,7 +89,7 @@ Example | File | Description `move_sprite` | [`2d/move_sprite.rs`](./2d/move_sprite.rs) | Changes the transform of a sprite. `mesh2d` | [`2d/mesh2d.rs`](./2d/mesh2d.rs) | Renders a 2d mesh `mesh2d_manual` | [`2d/mesh2d_manual.rs`](./2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis. -`rect` | [`2d/rect.rs`](./2d/rect.rs) | Renders a rectangle +`shapes` | [`2d/shapes.rs`](./2d/shapes.rs) | Renders a rectangle, circle, and hexagon `sprite` | [`2d/sprite.rs`](./2d/sprite.rs) | Renders a sprite `sprite_sheet` | [`2d/sprite_sheet.rs`](./2d/sprite_sheet.rs) | Renders an animated sprite `text2d` | [`2d/text2d.rs`](./2d/text2d.rs) | Generates text in 2d