bevy_render/mesh/primitives/dim3/
cone.rs

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