From 383314ef627da6654588a704959fc92c67770a52 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sun, 26 May 2024 00:56:09 +0300 Subject: [PATCH] Add meshing for `ConicalFrustum` (#11819) # Objective The `ConicalFrustum` primitive should support meshing. ## Solution Implement meshing for the `ConicalFrustum` primitive. The implementation is nearly identical to `Cylinder` meshing, but supports two radii. The default conical frustum is equivalent to a cone with a height of 1 and a radius of 0.5, truncated at half-height. ![kuva](https://github.com/bevyengine/bevy/assets/57632562/b4cab136-ff55-4056-b818-1218e4f38845) --- crates/bevy_math/src/primitives/dim3.rs | 11 + .../mesh/primitives/dim3/conical_frustum.rs | 190 ++++++++++++++++++ .../src/mesh/primitives/dim3/cylinder.rs | 2 - .../src/mesh/primitives/dim3/mod.rs | 2 + examples/3d/3d_shapes.rs | 3 +- 5 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 crates/bevy_render/src/mesh/primitives/dim3/conical_frustum.rs diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 2ad71c689d011..a6449cc2e0dd7 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -599,6 +599,17 @@ pub struct ConicalFrustum { } impl Primitive3d for ConicalFrustum {} +impl Default for ConicalFrustum { + /// Returns the default [`ConicalFrustum`] with a top radius of `0.25`, bottom radius of `0.5`, and a height of `0.5`. + fn default() -> Self { + Self { + radius_top: 0.25, + radius_bottom: 0.5, + height: 0.5, + } + } +} + /// The type of torus determined by the minor and major radii #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TorusKind { diff --git a/crates/bevy_render/src/mesh/primitives/dim3/conical_frustum.rs b/crates/bevy_render/src/mesh/primitives/dim3/conical_frustum.rs new file mode 100644 index 0000000000000..3fb020c1efd9d --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/conical_frustum.rs @@ -0,0 +1,190 @@ +use crate::{ + mesh::{Indices, Mesh, Meshable}, + render_asset::RenderAssetUsages, +}; +use bevy_math::{primitives::ConicalFrustum, Vec3}; +use wgpu::PrimitiveTopology; + +/// A builder used for creating a [`Mesh`] with a [`ConicalFrustum`] shape. +#[derive(Clone, Copy, Debug)] +pub struct ConicalFrustumMeshBuilder { + /// The [`ConicalFrustum`] shape. + pub frustum: ConicalFrustum, + /// The number of vertices used for the top and bottom of the conical frustum. + /// + /// The default is `32`. + pub resolution: u32, + /// The number of horizontal lines subdividing the lateral surface of the conical frustum. + /// + /// The default is `1`. + pub segments: u32, +} + +impl Default for ConicalFrustumMeshBuilder { + fn default() -> Self { + Self { + frustum: ConicalFrustum::default(), + resolution: 32, + segments: 1, + } + } +} + +impl ConicalFrustumMeshBuilder { + /// Creates a new [`ConicalFrustumMeshBuilder`] from the given top and bottom radii, a height, + /// and a resolution used for the top and bottom. + #[inline] + pub const fn new(radius_top: f32, radius_bottom: f32, height: f32, resolution: u32) -> Self { + Self { + frustum: ConicalFrustum { + radius_top, + radius_bottom, + height, + }, + resolution, + segments: 1, + } + } + + /// Sets the number of vertices used for the top and bottom of the conical frustum. + #[inline] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } + + /// Sets the number of horizontal lines subdividing the lateral surface of the conical frustum. + #[inline] + pub const fn segments(mut self, segments: u32) -> Self { + self.segments = segments; + self + } + + /// Builds a [`Mesh`] based on the configuration in `self`. + pub fn build(&self) -> Mesh { + debug_assert!(self.resolution > 2); + debug_assert!(self.segments > 0); + + let ConicalFrustum { + radius_top, + radius_bottom, + height, + } = self.frustum; + let half_height = height / 2.0; + + let num_rings = self.segments + 1; + let num_vertices = (self.resolution * 2 + num_rings * (self.resolution + 1)) as usize; + let num_faces = self.resolution * (num_rings - 2); + let num_indices = ((2 * num_faces + 2 * (self.resolution - 1) * 2) * 3) as usize; + + let mut positions = Vec::with_capacity(num_vertices); + let mut normals = Vec::with_capacity(num_vertices); + let mut uvs = Vec::with_capacity(num_vertices); + let mut indices = Vec::with_capacity(num_indices); + + let step_theta = std::f32::consts::TAU / self.resolution as f32; + let step_y = height / self.segments as f32; + let step_radius = (radius_top - radius_bottom) / self.segments as f32; + + // Rings + for ring in 0..num_rings { + let y = -half_height + ring as f32 * step_y; + let radius = radius_bottom + ring as f32 * step_radius; + + for segment in 0..=self.resolution { + let theta = segment as f32 * step_theta; + let (sin, cos) = theta.sin_cos(); + + positions.push([radius * cos, y, radius * sin]); + normals.push( + Vec3::new(cos, (radius_bottom - radius_top) / height, sin) + .normalize() + .to_array(), + ); + uvs.push([ + segment as f32 / self.resolution as f32, + ring as f32 / self.segments as f32, + ]); + } + } + + // Lateral surface + for i in 0..self.segments { + let ring = i * (self.resolution + 1); + let next_ring = (i + 1) * (self.resolution + 1); + + for j in 0..self.resolution { + indices.extend_from_slice(&[ + ring + j, + next_ring + j, + ring + j + 1, + next_ring + j, + next_ring + j + 1, + ring + j + 1, + ]); + } + } + + // Caps + let mut build_cap = |top: bool, radius: f32| { + let offset = positions.len() as u32; + let (y, normal_y, winding) = if top { + (half_height, 1.0, (1, 0)) + } else { + (-half_height, -1.0, (0, 1)) + }; + + for i in 0..self.resolution { + let theta = i as f32 * step_theta; + let (sin, cos) = theta.sin_cos(); + + positions.push([cos * radius, y, sin * radius]); + normals.push([0.0, normal_y, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + for i in 1..(self.resolution - 1) { + indices.extend_from_slice(&[ + offset, + offset + i + winding.0, + offset + i + winding.1, + ]); + } + }; + + build_cap(true, radius_top); + build_cap(false, radius_bottom); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for ConicalFrustum { + type Output = ConicalFrustumMeshBuilder; + + fn mesh(&self) -> Self::Output { + ConicalFrustumMeshBuilder { + frustum: *self, + ..Default::default() + } + } +} + +impl From for Mesh { + fn from(frustum: ConicalFrustum) -> Self { + frustum.mesh().build() + } +} + +impl From for Mesh { + fn from(frustum: ConicalFrustumMeshBuilder) -> Self { + frustum.build() + } +} diff --git a/crates/bevy_render/src/mesh/primitives/dim3/cylinder.rs b/crates/bevy_render/src/mesh/primitives/dim3/cylinder.rs index ba7bf03d3659e..22a9234788463 100644 --- a/crates/bevy_render/src/mesh/primitives/dim3/cylinder.rs +++ b/crates/bevy_render/src/mesh/primitives/dim3/cylinder.rs @@ -145,8 +145,6 @@ impl MeshBuilder for CylinderMeshBuilder { } }; - // top - build_cap(true); build_cap(false); diff --git a/crates/bevy_render/src/mesh/primitives/dim3/mod.rs b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs index a43b2d1862a0d..65b4eac7f6be3 100644 --- a/crates/bevy_render/src/mesh/primitives/dim3/mod.rs +++ b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs @@ -1,5 +1,6 @@ mod capsule; mod cone; +mod conical_frustum; mod cuboid; mod cylinder; mod plane; @@ -10,6 +11,7 @@ pub(crate) mod triangle3d; pub use capsule::*; pub use cone::*; +pub use conical_frustum::*; pub use cylinder::*; pub use plane::*; pub use sphere::*; diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index 183536b04dce7..9a92dcd3ce37f 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -24,7 +24,7 @@ fn main() { #[derive(Component)] struct Shape; -const X_EXTENT: f32 = 12.0; +const X_EXTENT: f32 = 14.0; fn setup( mut commands: Commands, @@ -44,6 +44,7 @@ fn setup( meshes.add(Torus::default()), meshes.add(Cylinder::default()), meshes.add(Cone::default()), + meshes.add(ConicalFrustum::default()), meshes.add(Sphere::default().mesh().ico(5).unwrap()), meshes.add(Sphere::default().mesh().uv(32, 18)), ];