Add Meshable trait and implement meshing for 2D primitives (#11431)

# Objective

The first part of #10569, split up from #11007.

The goal is to implement meshing support for Bevy's new geometric
primitives, starting with 2D primitives. 3D meshing will be added in a
follow-up, and we can consider removing the old mesh shapes completely.

## Solution

Add a `Meshable` trait that primitives need to implement to support
meshing, as suggested by the
[RFC](https://github.com/bevyengine/rfcs/blob/main/rfcs/12-primitive-shapes.md#meshing).

```rust
/// A trait for shapes that can be turned into a [`Mesh`].
pub trait Meshable {
    /// The output of [`Self::mesh`]. This can either be a [`Mesh`]
    /// or a builder used for creating a [`Mesh`].
    type Output;

    /// Creates a [`Mesh`] for a shape.
    fn mesh(&self) -> Self::Output;
}
```

This PR implements it for the following primitives:

- `Circle`
- `Ellipse`
- `Rectangle`
- `RegularPolygon`
- `Triangle2d`

The `mesh` method typically returns a builder-like struct such as
`CircleMeshBuilder`. This is needed to support shape-specific
configuration for things like mesh resolution or UV configuration:

```rust
meshes.add(Circle { radius: 0.5 }.mesh().resolution(64));
```

Note that if no configuration is needed, you can even skip calling
`mesh` because `From<MyPrimitive>` is implemented for `Mesh`:

```rust
meshes.add(Circle { radius: 0.5 });
```

I also updated the `2d_shapes` example to use primitives, and tweaked
the colors to have better contrast against the dark background.

Before:

![Old 2D
shapes](https://github.com/bevyengine/bevy/assets/57632562/f1d8c2d5-55be-495f-8ed4-5890154b81ca)

After:

![New 2D
shapes](https://github.com/bevyengine/bevy/assets/57632562/f166c013-34b8-4752-800a-5517b284d978)

Here you can see the UVs and different facing directions: (taken from
#11007, so excuse the 3D primitives at the bottom left)

![UVs and facing
directions](https://github.com/bevyengine/bevy/assets/57632562/eaf0be4e-187d-4b6d-8fb8-c996ba295a8a)

---

## Changelog

- Added `bevy_render::mesh::primitives` module
- Added `Meshable` trait and implemented it for:
  - `Circle`
  - `Ellipse`
  - `Rectangle`
  - `RegularPolygon`
  - `Triangle2d`
- Implemented `Default` and `Copy` for several 2D primitives
- Updated `2d_shapes` example to use primitives
- Tweaked colors in `2d_shapes` example to have better contrast against
the (new-ish) dark background

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Joona Aalto 2024-01-29 18:47:47 +02:00 committed by GitHub
parent 149a313850
commit 2bf481c03b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 393 additions and 24 deletions

View file

@ -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

View file

@ -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
///

View file

@ -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},

View file

@ -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};

View file

@ -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<Circle> for Mesh {
fn from(circle: Circle) -> Self {
circle.mesh().build()
}
}
impl From<CircleMeshBuilder> 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<RegularPolygon> 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<Ellipse> for Mesh {
fn from(ellipse: Ellipse) -> Self {
ellipse.mesh().build()
}
}
impl From<EllipseMeshBuilder> 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<Triangle2d> 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<Rectangle> for Mesh {
fn from(rectangle: Rectangle) -> Self {
rectangle.mesh()
}
}

View file

@ -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<Assets<Mesh>>) {
//! // 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;
}

View file

@ -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()
});
}

View file

@ -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