From c8aa3ac7d1867c554ba77411c29347bc687a2254 Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 1 Apr 2024 17:55:49 -0400 Subject: [PATCH] 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: Screenshot 2024-03-26 at 10 39 48 AM Other features are essentially identical to the implementations for `Circle`/`Ellipse`. --- ## Changelog - Introduced `AnnulusMeshBuilder` - Implemented `Meshable` for `Annulus` with `Output = AnnulusMeshBuilder` - Implemented `From` and `From` 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 --- .../src/impls/math/primitives2d.rs | 11 +- .../src/impls/math/primitives3d.rs | 8 ++ .../bevy_render/src/mesh/primitives/dim2.rs | 122 +++++++++++++++++- 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/crates/bevy_reflect/src/impls/math/primitives2d.rs b/crates/bevy_reflect/src/impls/math/primitives2d.rs index 03364fb5f2..c9d9a5b50d 100644 --- a/crates/bevy_reflect/src/impls/math/primitives2d.rs +++ b/crates/bevy_reflect/src/impls/math/primitives2d.rs @@ -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, } ); diff --git a/crates/bevy_reflect/src/impls/math/primitives3d.rs b/crates/bevy_reflect/src/impls/math/primitives3d.rs index e46f1ef78f..6b6f773a17 100644 --- a/crates/bevy_reflect/src/impls/math/primitives3d.rs +++ b/crates/bevy_reflect/src/impls/math/primitives3d.rs @@ -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"] diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 40e0186026..e10d7c845c 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -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 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 for Mesh { + fn from(annulus: Annulus) -> Self { + annulus.mesh().build() + } +} + +impl From for Mesh { + fn from(builder: AnnulusMeshBuilder) -> Self { + builder.build() + } +} + impl Meshable for Triangle2d { type Output = Mesh;