Meshing for Annulus primitive (#12734)

# Objective

Related to #10572 
Allow the `Annulus` primitive to be meshed.

## Solution

We introduce a `Meshable` structure, `AnnulusMeshBuilder`, which allows
the `Annulus` primitive to be meshed, leaving optional configuration of
the number of angular sudivisions to the user. Here is a picture of the
annulus's UV-mapping:
<img width="1440" alt="Screenshot 2024-03-26 at 10 39 48 AM"
src="https://github.com/bevyengine/bevy/assets/2975848/b170291d-cba7-441b-90ee-2ad6841eaedb">

Other features are essentially identical to the implementations for
`Circle`/`Ellipse`.

---

## Changelog

- Introduced `AnnulusMeshBuilder`
- Implemented `Meshable` for `Annulus` with `Output =
AnnulusMeshBuilder`
- Implemented `From<Annulus>` and `From<AnnulusMeshBuilder>` for `Mesh`
- Added `impl_reflect!` declaration for `Annulus` and `Triangle3d` in
`bevy_reflect`

---

## Discussion

### Design considerations

The only interesting wrinkle here is that the existing UV-mapping of
`Ellipse` (and hence of `Circle` and `RegularPolygon`) is non-radial
(it's skew-free, created by situating the mesh in a bounding rectangle),
so the UV-mapping of `Annulus` doesn't limit to that of `Circle` as its
inner radius tends to zero, for instance. I don't see this as a real
issue for `Annulus`, which should almost certainly have this kind of
UV-mapping, but I think we ought to at least consider allowing mesh
configuration for `Circle`/`Ellipse` that performs radial UV-mapping
instead. (In these cases in particular, it would be especially easy,
since we wouldn't need a different parameter set in the builder.)

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Matty 2024-04-01 17:55:49 -04:00 committed by GitHub
parent 20ee56e719
commit c8aa3ac7d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 139 additions and 2 deletions

View file

@ -15,7 +15,16 @@ impl_reflect!(
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "bevy_math::primitives"]
struct Ellipse {
pub half_size: Vec2,
half_size: Vec2,
}
);
impl_reflect!(
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "bevy_math::primitives"]
struct Annulus {
inner_circle: Circle,
outer_circle: Circle,
}
);

View file

@ -44,6 +44,14 @@ impl_reflect!(
}
);
impl_reflect!(
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "bevy_math::primitives"]
struct Triangle3d {
vertices: [Vec3; 3],
}
);
impl_reflect!(
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "bevy_math::primitives"]

View file

@ -5,7 +5,9 @@ use crate::{
use super::Meshable;
use bevy_math::{
primitives::{Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder},
primitives::{
Annulus, Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder,
},
Vec2,
};
use wgpu::PrimitiveTopology;
@ -193,6 +195,124 @@ impl From<EllipseMeshBuilder> for Mesh {
}
}
/// A builder for creating a [`Mesh`] with an [`Annulus`] shape.
pub struct AnnulusMeshBuilder {
/// The [`Annulus`] shape.
pub annulus: Annulus,
/// The number of vertices used in constructing each concentric circle of the annulus mesh.
/// The default is `32`.
pub resolution: usize,
}
impl Default for AnnulusMeshBuilder {
fn default() -> Self {
Self {
annulus: Annulus::default(),
resolution: 32,
}
}
}
impl AnnulusMeshBuilder {
/// Create an [`AnnulusMeshBuilder`] with the given inner radius, outer radius, and angular vertex count.
#[inline]
pub fn new(inner_radius: f32, outer_radius: f32, resolution: usize) -> Self {
Self {
annulus: Annulus::new(inner_radius, outer_radius),
resolution,
}
}
/// Sets the number of vertices used in constructing the concentric circles of the annulus mesh.
#[inline]
pub 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 inner_radius = self.annulus.inner_circle.radius;
let outer_radius = self.annulus.outer_circle.radius;
let num_vertices = (self.resolution + 1) * 2;
let mut indices = Vec::with_capacity(self.resolution * 6);
let mut positions = Vec::with_capacity(num_vertices);
let mut uvs = Vec::with_capacity(num_vertices);
let normals = vec![[0.0, 0.0, 1.0]; num_vertices];
// We have one more set of vertices than might be naïvely expected;
// the vertices at `start_angle` are duplicated for the purposes of UV
// mapping. Here, each iteration places a pair of vertices at a fixed
// angle from the center of the annulus.
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 {
let theta = start_angle + i as f32 * step;
let (sin, cos) = theta.sin_cos();
let inner_pos = [cos * inner_radius, sin * inner_radius, 0.];
let outer_pos = [cos * outer_radius, sin * outer_radius, 0.];
positions.push(inner_pos);
positions.push(outer_pos);
// The first UV direction is radial and the second is angular;
// i.e., a single UV rectangle is stretched around the annulus, with
// its top and bottom meeting as the circle closes. Lines of constant
// U map to circles, and lines of constant V map to radial line segments.
let inner_uv = [0., i as f32 / self.resolution as f32];
let outer_uv = [1., i as f32 / self.resolution as f32];
uvs.push(inner_uv);
uvs.push(outer_uv);
}
// Adjacent pairs of vertices form two triangles with each other; here,
// we are just making sure that they both have the right orientation,
// which is the CCW order of
// `inner_vertex` -> `outer_vertex` -> `next_outer` -> `next_inner`
for i in 0..(self.resolution as u32) {
let inner_vertex = 2 * i;
let outer_vertex = 2 * i + 1;
let next_inner = inner_vertex + 2;
let next_outer = outer_vertex + 2;
indices.extend_from_slice(&[inner_vertex, outer_vertex, next_outer]);
indices.extend_from_slice(&[next_outer, next_inner, inner_vertex]);
}
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_inserted_indices(Indices::U32(indices))
}
}
impl Meshable for Annulus {
type Output = AnnulusMeshBuilder;
fn mesh(&self) -> Self::Output {
AnnulusMeshBuilder {
annulus: *self,
..Default::default()
}
}
}
impl From<Annulus> for Mesh {
fn from(annulus: Annulus) -> Self {
annulus.mesh().build()
}
}
impl From<AnnulusMeshBuilder> for Mesh {
fn from(builder: AnnulusMeshBuilder) -> Self {
builder.build()
}
}
impl Meshable for Triangle2d {
type Output = Mesh;