bevy_mesh/primitives/dim3/
cone.rs

1use crate::{Indices, Mesh, MeshBuilder, Meshable};
2use bevy_asset::RenderAssetUsages;
3use bevy_math::{ops, primitives::Cone, Vec3};
4use wgpu::PrimitiveTopology;
5
6/// Anchoring options for [`ConeMeshBuilder`]
7#[derive(Debug, Copy, Clone, Default)]
8pub enum ConeAnchor {
9    #[default]
10    /// Midpoint between the tip of the cone and the center of its base.
11    MidPoint,
12    /// The Tip of the triangle
13    Tip,
14    /// The center of the base circle
15    Base,
16}
17
18/// A builder used for creating a [`Mesh`] with a [`Cone`] shape.
19#[derive(Clone, Copy, Debug)]
20pub struct ConeMeshBuilder {
21    /// The [`Cone`] shape.
22    pub cone: Cone,
23    /// The number of vertices used for the base of the cone.
24    ///
25    /// The default is `32`.
26    pub resolution: u32,
27    /// The anchor point for the cone mesh, defaults to the midpoint between
28    /// the tip of the cone and the center of its base
29    pub anchor: ConeAnchor,
30}
31
32impl Default for ConeMeshBuilder {
33    fn default() -> Self {
34        Self {
35            cone: Cone::default(),
36            resolution: 32,
37            anchor: ConeAnchor::default(),
38        }
39    }
40}
41
42impl ConeMeshBuilder {
43    /// Creates a new [`ConeMeshBuilder`] from a given radius, height,
44    /// and number of vertices used for the base of the cone.
45    #[inline]
46    pub const fn new(radius: f32, height: f32, resolution: u32) -> Self {
47        Self {
48            cone: Cone { radius, height },
49            resolution,
50            anchor: ConeAnchor::MidPoint,
51        }
52    }
53
54    /// Sets the number of vertices used for the base of the cone.
55    #[inline]
56    pub const fn resolution(mut self, resolution: u32) -> Self {
57        self.resolution = resolution;
58        self
59    }
60
61    /// Sets a custom anchor point for the mesh
62    #[inline]
63    pub const fn anchor(mut self, anchor: ConeAnchor) -> Self {
64        self.anchor = anchor;
65        self
66    }
67}
68
69impl MeshBuilder for ConeMeshBuilder {
70    fn build(&self) -> Mesh {
71        let half_height = self.cone.height / 2.0;
72
73        // `resolution` vertices for the base, `resolution` vertices for the bottom of the lateral surface,
74        // and one vertex for the tip.
75        let num_vertices = self.resolution as usize * 2 + 1;
76        let num_indices = self.resolution as usize * 6 - 6;
77
78        let mut positions = Vec::with_capacity(num_vertices);
79        let mut normals = Vec::with_capacity(num_vertices);
80        let mut uvs = Vec::with_capacity(num_vertices);
81        let mut indices = Vec::with_capacity(num_indices);
82
83        // Tip
84        positions.push([0.0, half_height, 0.0]);
85
86        // The tip doesn't have a singular normal that works correctly.
87        // We use an invalid normal here so that it becomes NaN in the fragment shader
88        // and doesn't affect the overall shading. This might seem hacky, but it's one of
89        // the only ways to get perfectly smooth cones without creases or other shading artifacts.
90        //
91        // Note that this requires that normals are not normalized in the vertex shader,
92        // as that would make the entire triangle invalid and make the cone appear as black.
93        normals.push([0.0, 0.0, 0.0]);
94
95        // The UVs of the cone are in polar coordinates, so it's like projecting a circle texture from above.
96        // The center of the texture is at the center of the lateral surface, at the tip of the cone.
97        uvs.push([0.5, 0.5]);
98
99        // Now we build the lateral surface, the side of the cone.
100
101        // The vertex normals will be perpendicular to the surface.
102        //
103        // Here we get the slope of a normal and use it for computing
104        // the multiplicative inverse of the length of a vector in the direction
105        // of the normal. This allows us to normalize vertex normals efficiently.
106        let normal_slope = self.cone.radius / self.cone.height;
107        // Equivalent to Vec2::new(1.0, slope).length().recip()
108        let normalization_factor = (1.0 + normal_slope * normal_slope).sqrt().recip();
109
110        // How much the angle changes at each step
111        let step_theta = core::f32::consts::TAU / self.resolution as f32;
112
113        // Add vertices for the bottom of the lateral surface.
114        for segment in 0..self.resolution {
115            let theta = segment as f32 * step_theta;
116            let (sin, cos) = ops::sin_cos(theta);
117
118            // The vertex normal perpendicular to the side
119            let normal = Vec3::new(cos, normal_slope, sin) * normalization_factor;
120
121            positions.push([self.cone.radius * cos, -half_height, self.cone.radius * sin]);
122            normals.push(normal.to_array());
123            uvs.push([0.5 + cos * 0.5, 0.5 + sin * 0.5]);
124        }
125
126        // Add indices for the lateral surface. Each triangle is formed by the tip
127        // and two vertices at the base.
128        for j in 1..self.resolution {
129            indices.extend_from_slice(&[0, j + 1, j]);
130        }
131
132        // Close the surface with a triangle between the tip, first base vertex, and last base vertex.
133        indices.extend_from_slice(&[0, 1, self.resolution]);
134
135        // Now we build the actual base of the cone.
136
137        let index_offset = positions.len() as u32;
138
139        // Add base vertices.
140        for i in 0..self.resolution {
141            let theta = i as f32 * step_theta;
142            let (sin, cos) = ops::sin_cos(theta);
143
144            positions.push([cos * self.cone.radius, -half_height, sin * self.cone.radius]);
145            normals.push([0.0, -1.0, 0.0]);
146            uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]);
147        }
148
149        // Add base indices.
150        for i in 1..(self.resolution - 1) {
151            indices.extend_from_slice(&[index_offset, index_offset + i, index_offset + i + 1]);
152        }
153
154        // Offset the vertex positions Y axis to match the anchor
155        match self.anchor {
156            ConeAnchor::Tip => positions.iter_mut().for_each(|p| p[1] -= half_height),
157            ConeAnchor::Base => positions.iter_mut().for_each(|p| p[1] += half_height),
158            ConeAnchor::MidPoint => (),
159        };
160
161        Mesh::new(
162            PrimitiveTopology::TriangleList,
163            RenderAssetUsages::default(),
164        )
165        .with_inserted_indices(Indices::U32(indices))
166        .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
167        .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
168        .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
169    }
170}
171
172impl Meshable for Cone {
173    type Output = ConeMeshBuilder;
174
175    fn mesh(&self) -> Self::Output {
176        ConeMeshBuilder {
177            cone: *self,
178            ..Default::default()
179        }
180    }
181}
182
183impl From<Cone> for Mesh {
184    fn from(cone: Cone) -> Self {
185        cone.mesh().build()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use crate::{Mesh, MeshBuilder, Meshable, VertexAttributeValues};
192    use bevy_math::{primitives::Cone, Vec2};
193
194    /// Rounds floats to handle floating point error in tests.
195    fn round_floats<const N: usize>(points: &mut [[f32; N]]) {
196        for point in points.iter_mut() {
197            for coord in point.iter_mut() {
198                let round = (*coord * 100.0).round() / 100.0;
199                if (*coord - round).abs() < 0.00001 {
200                    *coord = round;
201                }
202            }
203        }
204    }
205
206    #[test]
207    fn cone_mesh() {
208        let mut mesh = Cone {
209            radius: 0.5,
210            height: 1.0,
211        }
212        .mesh()
213        .resolution(4)
214        .build();
215
216        let Some(VertexAttributeValues::Float32x3(mut positions)) =
217            mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION)
218        else {
219            panic!("Expected positions f32x3");
220        };
221        let Some(VertexAttributeValues::Float32x3(mut normals)) =
222            mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL)
223        else {
224            panic!("Expected normals f32x3");
225        };
226
227        round_floats(&mut positions);
228        round_floats(&mut normals);
229
230        // Vertex positions
231        assert_eq!(
232            [
233                // Tip
234                [0.0, 0.5, 0.0],
235                // Lateral surface
236                [0.5, -0.5, 0.0],
237                [0.0, -0.5, 0.5],
238                [-0.5, -0.5, 0.0],
239                [0.0, -0.5, -0.5],
240                // Base
241                [0.5, -0.5, 0.0],
242                [0.0, -0.5, 0.5],
243                [-0.5, -0.5, 0.0],
244                [0.0, -0.5, -0.5],
245            ],
246            &positions[..]
247        );
248
249        // Vertex normals
250        let [x, y] = Vec2::new(0.5, -1.0).perp().normalize().to_array();
251        assert_eq!(
252            &[
253                // Tip
254                [0.0, 0.0, 0.0],
255                // Lateral surface
256                [x, y, 0.0],
257                [0.0, y, x],
258                [-x, y, 0.0],
259                [0.0, y, -x],
260                // Base
261                [0.0, -1.0, 0.0],
262                [0.0, -1.0, 0.0],
263                [0.0, -1.0, 0.0],
264                [0.0, -1.0, 0.0],
265            ],
266            &normals[..]
267        );
268    }
269}