diff --git a/Cargo.toml b/Cargo.toml index eab962b73d..b764b16058 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -402,7 +402,7 @@ doc-scrape-examples = true [package.metadata.example.2d_shapes] name = "2D Shapes" -description = "Renders a rectangle, circle, and hexagon" +description = "Renders simple 2D primitive shapes like circles and polygons" category = "2D Rendering" wasm = true diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index c7b87afa68..906850a999 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -90,6 +90,13 @@ pub struct Circle { } impl Primitive2d for Circle {} +impl Default for Circle { + /// Returns the default [`Circle`] with a radius of `0.5`. + fn default() -> Self { + Self { radius: 0.5 } + } +} + impl Circle { /// Create a new [`Circle`] from a `radius` #[inline(always)] @@ -147,6 +154,15 @@ pub struct Ellipse { } impl Primitive2d for Ellipse {} +impl Default for Ellipse { + /// Returns the default [`Ellipse`] with a half-width of `1.0` and a half-height of `0.5`. + fn default() -> Self { + Self { + half_size: Vec2::new(1.0, 0.5), + } + } +} + impl Ellipse { /// Create a new `Ellipse` from half of its width and height. /// @@ -197,6 +213,15 @@ pub struct Plane2d { } impl Primitive2d for Plane2d {} +impl Default for Plane2d { + /// Returns the default [`Plane2d`] with a normal pointing in the `+Y` direction. + fn default() -> Self { + Self { + normal: Direction2d::Y, + } + } +} + impl Plane2d { /// Create a new `Plane2d` from a normal /// @@ -343,10 +368,19 @@ pub struct Triangle2d { } impl Primitive2d for Triangle2d {} +impl Default for Triangle2d { + /// Returns the default [`Triangle2d`] with the vertices `[0.0, 0.5]`, `[-0.5, -0.5]`, and `[0.5, -0.5]`. + fn default() -> Self { + Self { + vertices: [Vec2::Y * 0.5, Vec2::new(-0.5, -0.5), Vec2::new(0.5, -0.5)], + } + } +} + impl Triangle2d { /// Create a new `Triangle2d` from points `a`, `b`, and `c` #[inline(always)] - pub fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { + pub const fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { Self { vertices: [a, b, c], } @@ -438,6 +472,15 @@ pub struct Rectangle { pub half_size: Vec2, } +impl Default for Rectangle { + /// Returns the default [`Rectangle`] with a half-width and half-height of `0.5`. + fn default() -> Self { + Self { + half_size: Vec2::splat(0.5), + } + } +} + impl Rectangle { /// Create a new `Rectangle` from a full width and height #[inline(always)] @@ -559,9 +602,19 @@ pub struct RegularPolygon { } impl Primitive2d for RegularPolygon {} +impl Default for RegularPolygon { + /// Returns the default [`RegularPolygon`] with six sides (a hexagon) and a circumradius of `0.5`. + fn default() -> Self { + Self { + circumcircle: Circle { radius: 0.5 }, + sides: 6, + } + } +} + impl RegularPolygon { /// Create a new `RegularPolygon` - /// from the radius of the circumcircle and number of sides + /// from the radius of the circumcircle and a number of sides /// /// # Panics /// diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index cad006b34b..5d31c855a0 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -34,7 +34,7 @@ pub mod prelude { Projection, }, color::Color, - mesh::{morph::MorphWeights, shape, Mesh}, + mesh::{morph::MorphWeights, primitives::Meshable, shape, Mesh}, render_resource::Shader, spatial_bundle::SpatialBundle, texture::{Image, ImagePlugin}, diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 39c9b19cc9..0a070d0388 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -1,10 +1,12 @@ #[allow(clippy::module_inception)] mod mesh; pub mod morph; +pub mod primitives; /// Generation for some primitive shape meshes. pub mod shape; pub use mesh::*; +pub use primitives::*; use crate::{prelude::Image, render_asset::RenderAssetPlugin}; use bevy_app::{App, Plugin}; diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs new file mode 100644 index 0000000000..833aa0af49 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -0,0 +1,268 @@ +use crate::{ + mesh::{Indices, Mesh}, + render_asset::RenderAssetPersistencePolicy, +}; + +use super::Meshable; +use bevy_math::{ + primitives::{Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder}, + Vec2, +}; +use wgpu::PrimitiveTopology; + +/// A builder used for creating a [`Mesh`] with a [`Circle`] shape. +#[derive(Clone, Copy, Debug)] +pub struct CircleMeshBuilder { + /// The [`Circle`] shape. + pub circle: Circle, + /// The number of vertices used for the circle mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: usize, +} + +impl Default for CircleMeshBuilder { + fn default() -> Self { + Self { + circle: Circle::default(), + resolution: 32, + } + } +} + +impl CircleMeshBuilder { + /// Creates a new [`CircleMeshBuilder`] from a given radius and vertex count. + #[inline] + pub const fn new(radius: f32, resolution: usize) -> Self { + Self { + circle: Circle { radius }, + resolution, + } + } + + /// Sets the number of vertices used for the circle mesh. + #[inline] + #[doc(alias = "vertices")] + 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 { + RegularPolygon::new(self.circle.radius, self.resolution).mesh() + } +} + +impl Meshable for Circle { + type Output = CircleMeshBuilder; + + fn mesh(&self) -> Self::Output { + CircleMeshBuilder { + circle: *self, + ..Default::default() + } + } +} + +impl From for Mesh { + fn from(circle: Circle) -> Self { + circle.mesh().build() + } +} + +impl From for Mesh { + fn from(circle: CircleMeshBuilder) -> Self { + circle.build() + } +} + +impl Meshable for RegularPolygon { + type Output = Mesh; + + fn mesh(&self) -> Self::Output { + // The ellipse mesh is just a regular polygon with two radii + Ellipse::new(self.circumcircle.radius, self.circumcircle.radius) + .mesh() + .resolution(self.sides) + .build() + } +} + +impl From for Mesh { + fn from(polygon: RegularPolygon) -> Self { + polygon.mesh() + } +} + +/// A builder used for creating a [`Mesh`] with an [`Ellipse`] shape. +#[derive(Clone, Copy, Debug)] +pub struct EllipseMeshBuilder { + /// The [`Ellipse`] shape. + pub ellipse: Ellipse, + /// The number of vertices used for the ellipse mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: usize, +} + +impl Default for EllipseMeshBuilder { + fn default() -> Self { + Self { + ellipse: Ellipse::default(), + resolution: 32, + } + } +} + +impl EllipseMeshBuilder { + /// Creates a new [`EllipseMeshBuilder`] from a given half width and half height and a vertex count. + #[inline] + pub const fn new(half_width: f32, half_height: f32, resolution: usize) -> Self { + Self { + ellipse: Ellipse::new(half_width, half_height), + resolution, + } + } + + /// Sets the number of vertices used for the ellipse mesh. + #[inline] + #[doc(alias = "vertices")] + 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 { + let mut indices = Vec::with_capacity((self.resolution - 2) * 3); + let mut positions = Vec::with_capacity(self.resolution); + let normals = vec![[0.0, 0.0, 1.0]; self.resolution]; + let mut uvs = Vec::with_capacity(self.resolution); + + // Add pi/2 so that there is a vertex at the top (sin is 1.0 and cos is 0.0) + let start_angle = std::f32::consts::FRAC_PI_2; + let step = std::f32::consts::TAU / self.resolution as f32; + + for i in 0..self.resolution { + // Compute vertex position at angle theta + let theta = start_angle + i as f32 * step; + let (sin, cos) = theta.sin_cos(); + let x = cos * self.ellipse.half_size.x; + let y = sin * self.ellipse.half_size.y; + + positions.push([x, y, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + for i in 1..(self.resolution as u32 - 1) { + indices.extend_from_slice(&[0, i, i + 1]); + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .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 Ellipse { + type Output = EllipseMeshBuilder; + + fn mesh(&self) -> Self::Output { + EllipseMeshBuilder { + ellipse: *self, + ..Default::default() + } + } +} + +impl From for Mesh { + fn from(ellipse: Ellipse) -> Self { + ellipse.mesh().build() + } +} + +impl From for Mesh { + fn from(ellipse: EllipseMeshBuilder) -> Self { + ellipse.build() + } +} + +impl Meshable for Triangle2d { + type Output = Mesh; + + fn mesh(&self) -> Self::Output { + let [a, b, c] = self.vertices; + + let positions = vec![[a.x, a.y, 0.0], [b.x, b.y, 0.0], [c.x, c.y, 0.0]]; + let normals = vec![[0.0, 0.0, 1.0]; 3]; + + // The extents of the bounding box of the triangle, + // used to compute the UV coordinates of the points. + let extents = a.min(b).min(c).abs().max(a.max(b).max(c)) * Vec2::new(1.0, -1.0); + let uvs = vec![ + a / extents / 2.0 + 0.5, + b / extents / 2.0 + 0.5, + c / extents / 2.0 + 0.5, + ]; + + let is_ccw = self.winding_order() == WindingOrder::CounterClockwise; + let indices = if is_ccw { + Indices::U32(vec![0, 1, 2]) + } else { + Indices::U32(vec![0, 2, 1]) + }; + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl From for Mesh { + fn from(triangle: Triangle2d) -> Self { + triangle.mesh() + } +} + +impl Meshable for Rectangle { + type Output = Mesh; + + fn mesh(&self) -> Self::Output { + let [hw, hh] = [self.half_size.x, self.half_size.y]; + let positions = vec![ + [hw, hh, 0.0], + [-hw, hh, 0.0], + [-hw, -hh, 0.0], + [hw, -hh, 0.0], + ]; + let normals = vec![[0.0, 0.0, 1.0]; 4]; + let uvs = vec![[1.0, 0.0], [0.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; + let indices = Indices::U32(vec![0, 1, 2, 0, 2, 3]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl From for Mesh { + fn from(rectangle: Rectangle) -> Self { + rectangle.mesh() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/mod.rs b/crates/bevy_render/src/mesh/primitives/mod.rs new file mode 100644 index 0000000000..b05b3645cf --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/mod.rs @@ -0,0 +1,35 @@ +//! Mesh generation for [primitive shapes](bevy_math::primitives). +//! +//! Primitives that support meshing implement the [`Meshable`] trait. +//! Calling [`mesh`](Meshable::mesh) will return either a [`Mesh`](super::Mesh) or a builder +//! that can be used to specify shape-specific configuration for creating the [`Mesh`](super::Mesh). +//! +//! ``` +//! # use bevy_asset::Assets; +//! # use bevy_ecs::prelude::ResMut; +//! # use bevy_math::prelude::Circle; +//! # use bevy_render::prelude::*; +//! # +//! # fn setup(mut meshes: ResMut>) { +//! // Create circle mesh with default configuration +//! let circle = meshes.add(Circle { radius: 25.0 }); +//! +//! // Specify number of vertices +//! let circle = meshes.add(Circle { radius: 25.0 }.mesh().resolution(64)); +//! # } +//! ``` + +#![warn(missing_docs)] + +mod dim2; +pub use dim2::{CircleMeshBuilder, EllipseMeshBuilder}; + +/// A trait for shapes that can be turned into a [`Mesh`](super::Mesh). +pub trait Meshable { + /// The output of [`Self::mesh`]. This can either be a [`Mesh`](super::Mesh) + /// or a builder used for creating a [`Mesh`](super::Mesh). + type Output; + + /// Creates a [`Mesh`](super::Mesh) for a shape. + fn mesh(&self) -> Self::Output; +} diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 9a16f96ce5..6ac3736a53 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -18,36 +18,47 @@ fn setup( // Circle commands.spawn(MaterialMesh2dBundle { - mesh: meshes.add(shape::Circle::new(50.)).into(), - material: materials.add(Color::PURPLE), - transform: Transform::from_translation(Vec3::new(-150., 0., 0.)), + 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)), + ..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)), ..default() }); // Rectangle - commands.spawn(SpriteBundle { - sprite: Sprite { - color: Color::rgb(0.25, 0.25, 0.75), - custom_size: Some(Vec2::new(50.0, 100.0)), - ..default() - }, - transform: Transform::from_translation(Vec3::new(-50., 0., 0.)), - ..default() - }); - - // Quad commands.spawn(MaterialMesh2dBundle { - mesh: meshes.add(shape::Quad::new(Vec2::new(50., 100.))).into(), + mesh: meshes.add(Rectangle::new(50.0, 100.0)).into(), material: materials.add(Color::LIME_GREEN), - transform: Transform::from_translation(Vec3::new(50., 0., 0.)), + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), ..default() }); // Hexagon commands.spawn(MaterialMesh2dBundle { - mesh: meshes.add(shape::RegularPolygon::new(50., 6)).into(), - material: materials.add(Color::TURQUOISE), - transform: Transform::from_translation(Vec3::new(150., 0., 0.)), + 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)), + ..default() + }); + + // Triangle + commands.spawn(MaterialMesh2dBundle { + mesh: meshes + .add(Triangle2d::new( + Vec2::Y * 50.0, + Vec2::new(-50.0, -50.0), + Vec2::new(50.0, -50.0), + )) + .into(), + material: materials.add(Color::ORANGE), + transform: Transform::from_translation(Vec3::new(250.0, 0.0, 0.0)), ..default() }); } diff --git a/examples/README.md b/examples/README.md index 175fc632a4..e616e6c7a1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -95,7 +95,7 @@ Example | Description [2D Bloom](../examples/2d/bloom_2d.rs) | Illustrates bloom post-processing in 2d [2D Gizmos](../examples/2d/2d_gizmos.rs) | A scene showcasing 2D gizmos [2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions -[2D Shapes](../examples/2d/2d_shapes.rs) | Renders a rectangle, circle, and hexagon +[2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons [2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method [Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis