Skip to content

Commit c8aa3ac

Browse files
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 <[email protected]>
1 parent 20ee56e commit c8aa3ac

File tree

3 files changed

+139
-2
lines changed

3 files changed

+139
-2
lines changed

crates/bevy_reflect/src/impls/math/primitives2d.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ impl_reflect!(
1515
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
1616
#[type_path = "bevy_math::primitives"]
1717
struct Ellipse {
18-
pub half_size: Vec2,
18+
half_size: Vec2,
19+
}
20+
);
21+
22+
impl_reflect!(
23+
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
24+
#[type_path = "bevy_math::primitives"]
25+
struct Annulus {
26+
inner_circle: Circle,
27+
outer_circle: Circle,
1928
}
2029
);
2130

crates/bevy_reflect/src/impls/math/primitives3d.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ impl_reflect!(
4444
}
4545
);
4646

47+
impl_reflect!(
48+
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
49+
#[type_path = "bevy_math::primitives"]
50+
struct Triangle3d {
51+
vertices: [Vec3; 3],
52+
}
53+
);
54+
4755
impl_reflect!(
4856
#[reflect(Debug, PartialEq, Serialize, Deserialize)]
4957
#[type_path = "bevy_math::primitives"]

crates/bevy_render/src/mesh/primitives/dim2.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use crate::{
55

66
use super::Meshable;
77
use bevy_math::{
8-
primitives::{Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder},
8+
primitives::{
9+
Annulus, Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder,
10+
},
911
Vec2,
1012
};
1113
use wgpu::PrimitiveTopology;
@@ -193,6 +195,124 @@ impl From<EllipseMeshBuilder> for Mesh {
193195
}
194196
}
195197

198+
/// A builder for creating a [`Mesh`] with an [`Annulus`] shape.
199+
pub struct AnnulusMeshBuilder {
200+
/// The [`Annulus`] shape.
201+
pub annulus: Annulus,
202+
203+
/// The number of vertices used in constructing each concentric circle of the annulus mesh.
204+
/// The default is `32`.
205+
pub resolution: usize,
206+
}
207+
208+
impl Default for AnnulusMeshBuilder {
209+
fn default() -> Self {
210+
Self {
211+
annulus: Annulus::default(),
212+
resolution: 32,
213+
}
214+
}
215+
}
216+
217+
impl AnnulusMeshBuilder {
218+
/// Create an [`AnnulusMeshBuilder`] with the given inner radius, outer radius, and angular vertex count.
219+
#[inline]
220+
pub fn new(inner_radius: f32, outer_radius: f32, resolution: usize) -> Self {
221+
Self {
222+
annulus: Annulus::new(inner_radius, outer_radius),
223+
resolution,
224+
}
225+
}
226+
227+
/// Sets the number of vertices used in constructing the concentric circles of the annulus mesh.
228+
#[inline]
229+
pub fn resolution(mut self, resolution: usize) -> Self {
230+
self.resolution = resolution;
231+
self
232+
}
233+
234+
/// Builds a [`Mesh`] based on the configuration in `self`.
235+
pub fn build(&self) -> Mesh {
236+
let inner_radius = self.annulus.inner_circle.radius;
237+
let outer_radius = self.annulus.outer_circle.radius;
238+
239+
let num_vertices = (self.resolution + 1) * 2;
240+
let mut indices = Vec::with_capacity(self.resolution * 6);
241+
let mut positions = Vec::with_capacity(num_vertices);
242+
let mut uvs = Vec::with_capacity(num_vertices);
243+
let normals = vec![[0.0, 0.0, 1.0]; num_vertices];
244+
245+
// We have one more set of vertices than might be naïvely expected;
246+
// the vertices at `start_angle` are duplicated for the purposes of UV
247+
// mapping. Here, each iteration places a pair of vertices at a fixed
248+
// angle from the center of the annulus.
249+
let start_angle = std::f32::consts::FRAC_PI_2;
250+
let step = std::f32::consts::TAU / self.resolution as f32;
251+
for i in 0..=self.resolution {
252+
let theta = start_angle + i as f32 * step;
253+
let (sin, cos) = theta.sin_cos();
254+
let inner_pos = [cos * inner_radius, sin * inner_radius, 0.];
255+
let outer_pos = [cos * outer_radius, sin * outer_radius, 0.];
256+
positions.push(inner_pos);
257+
positions.push(outer_pos);
258+
259+
// The first UV direction is radial and the second is angular;
260+
// i.e., a single UV rectangle is stretched around the annulus, with
261+
// its top and bottom meeting as the circle closes. Lines of constant
262+
// U map to circles, and lines of constant V map to radial line segments.
263+
let inner_uv = [0., i as f32 / self.resolution as f32];
264+
let outer_uv = [1., i as f32 / self.resolution as f32];
265+
uvs.push(inner_uv);
266+
uvs.push(outer_uv);
267+
}
268+
269+
// Adjacent pairs of vertices form two triangles with each other; here,
270+
// we are just making sure that they both have the right orientation,
271+
// which is the CCW order of
272+
// `inner_vertex` -> `outer_vertex` -> `next_outer` -> `next_inner`
273+
for i in 0..(self.resolution as u32) {
274+
let inner_vertex = 2 * i;
275+
let outer_vertex = 2 * i + 1;
276+
let next_inner = inner_vertex + 2;
277+
let next_outer = outer_vertex + 2;
278+
indices.extend_from_slice(&[inner_vertex, outer_vertex, next_outer]);
279+
indices.extend_from_slice(&[next_outer, next_inner, inner_vertex]);
280+
}
281+
282+
Mesh::new(
283+
PrimitiveTopology::TriangleList,
284+
RenderAssetUsages::default(),
285+
)
286+
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
287+
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
288+
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
289+
.with_inserted_indices(Indices::U32(indices))
290+
}
291+
}
292+
293+
impl Meshable for Annulus {
294+
type Output = AnnulusMeshBuilder;
295+
296+
fn mesh(&self) -> Self::Output {
297+
AnnulusMeshBuilder {
298+
annulus: *self,
299+
..Default::default()
300+
}
301+
}
302+
}
303+
304+
impl From<Annulus> for Mesh {
305+
fn from(annulus: Annulus) -> Self {
306+
annulus.mesh().build()
307+
}
308+
}
309+
310+
impl From<AnnulusMeshBuilder> for Mesh {
311+
fn from(builder: AnnulusMeshBuilder) -> Self {
312+
builder.build()
313+
}
314+
}
315+
196316
impl Meshable for Triangle2d {
197317
type Output = Mesh;
198318

0 commit comments

Comments
 (0)