Skip to main content

bevy_mesh/primitives/dim3/
sphere.rs

1use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology};
2use bevy_asset::RenderAssetUsages;
3use bevy_math::ops;
4use bevy_reflect::prelude::*;
5use bevy_shape::Sphere;
6use core::f32::consts::PI;
7use hexasphere::shapes::IcoSphere;
8use thiserror::Error;
9
10/// An error when creating an icosphere [`Mesh`] from a [`SphereMeshBuilder`].
11#[derive(Clone, Copy, Debug, Error)]
12pub enum IcosphereError {
13    /// The icosphere has too many vertices.
14    #[error("Cannot create an icosphere of {subdivisions} subdivisions due to there being too many vertices being generated: {number_of_resulting_points}. (Limited to 65535 vertices or 79 subdivisions)")]
15    TooManyVertices {
16        /// The number of subdivisions used. 79 is the largest allowed value for a mesh to be generated.
17        subdivisions: u32,
18        /// The number of vertices generated. 65535 is the largest allowed value for a mesh to be generated.
19        number_of_resulting_points: u32,
20    },
21}
22
23/// A type of sphere mesh.
24#[derive(Clone, Copy, Debug, Reflect)]
25#[reflect(Default, Debug, Clone)]
26pub enum SphereKind {
27    /// An icosphere, a spherical mesh that consists of similar sized triangles.
28    Ico {
29        /// The number of subdivisions applied.
30        /// The number of faces quadruples with each subdivision.
31        subdivisions: u32,
32    },
33    /// A UV sphere, a spherical mesh that consists of quadrilaterals
34    /// apart from triangles at the top and bottom.
35    Uv {
36        /// The number of longitudinal sectors, aka the horizontal resolution.
37        #[doc(alias = "horizontal_resolution")]
38        sectors: u32,
39        /// The number of latitudinal stacks, aka the vertical resolution.
40        #[doc(alias = "vertical_resolution")]
41        stacks: u32,
42    },
43}
44
45impl Default for SphereKind {
46    fn default() -> Self {
47        Self::Ico { subdivisions: 5 }
48    }
49}
50
51/// A builder used for creating a [`Mesh`] with an [`Sphere`] shape.
52#[derive(Clone, Copy, Debug, Default, Reflect)]
53#[reflect(Default, Debug, Clone)]
54pub struct SphereMeshBuilder {
55    /// The [`Sphere`] shape.
56    pub sphere: Sphere,
57    /// The type of sphere mesh that will be built.
58    pub kind: SphereKind,
59}
60
61impl SphereMeshBuilder {
62    /// Creates a new [`SphereMeshBuilder`] from a radius and [`SphereKind`].
63    #[inline]
64    pub const fn new(radius: f32, kind: SphereKind) -> Self {
65        Self {
66            sphere: Sphere { radius },
67            kind,
68        }
69    }
70
71    /// Sets the [`SphereKind`] that will be used for building the mesh.
72    #[inline]
73    pub const fn kind(mut self, kind: SphereKind) -> Self {
74        self.kind = kind;
75        self
76    }
77
78    /// Creates an icosphere mesh with the given number of subdivisions.
79    ///
80    /// The number of faces quadruples with each subdivision.
81    /// If there are `80` or more subdivisions, the vertex count will be too large,
82    /// and an [`IcosphereError`] is returned.
83    ///
84    /// A good default is `5` subdivisions.
85    pub fn ico(&self, subdivisions: u32) -> Result<Mesh, IcosphereError> {
86        if subdivisions >= 80 {
87            /*
88            Number of triangles:
89            N = 20
90
91            Number of edges:
92            E = 30
93
94            Number of vertices:
95            V = 12
96
97            Number of points within a triangle (triangular numbers):
98            inner(s) = (s^2 + s) / 2
99
100            Number of points on an edge:
101            edges(s) = s
102
103            Add up all vertices on the surface:
104            vertices(s) = edges(s) * E + inner(s - 1) * N + V
105
106            Expand and simplify. Notice that the triangular number formula has roots at -1, and 0, so translating it one to the right fixes it.
107            subdivisions(s) = 30s + 20((s^2 - 2s + 1 + s - 1) / 2) + 12
108            subdivisions(s) = 30s + 10s^2 - 10s + 12
109            subdivisions(s) = 10(s^2 + 2s) + 12
110
111            Factor an (s + 1) term to simplify in terms of calculation
112            subdivisions(s) = 10(s + 1)^2 + 12 - 10
113            resulting_vertices(s) = 10(s + 1)^2 + 2
114            */
115            let temp = subdivisions + 1;
116            let number_of_resulting_points = temp * temp * 10 + 2;
117            return Err(IcosphereError::TooManyVertices {
118                subdivisions,
119                number_of_resulting_points,
120            });
121        }
122        let generated = IcoSphere::new(subdivisions as usize, |point| {
123            let inclination = ops::acos(point.y);
124            let azimuth = ops::atan2(point.z, point.x);
125
126            let norm_inclination = inclination / PI;
127            let norm_azimuth = 0.5 - (azimuth / core::f32::consts::TAU);
128
129            [norm_azimuth, norm_inclination]
130        });
131
132        let raw_points = generated.raw_points();
133
134        let points = raw_points
135            .iter()
136            .map(|&p| (p * self.sphere.radius).into())
137            .collect::<Vec<[f32; 3]>>();
138
139        let normals = raw_points
140            .iter()
141            .copied()
142            .map(Into::into)
143            .collect::<Vec<[f32; 3]>>();
144
145        let uvs = generated.raw_data().to_owned();
146
147        let mut indices = Vec::with_capacity(generated.indices_per_main_triangle() * 20);
148
149        for i in 0..20 {
150            generated.get_indices(i, &mut indices);
151        }
152
153        let indices = Indices::U32(indices);
154
155        Ok(Mesh::new(
156            PrimitiveTopology::TriangleList,
157            RenderAssetUsages::default(),
158        )
159        .with_inserted_indices(indices)
160        .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points)
161        .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
162        .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs))
163    }
164
165    /// Creates a UV sphere [`Mesh`] with the given number of
166    /// longitudinal sectors and latitudinal stacks, aka horizontal and vertical resolution.
167    ///
168    /// A good default is `32` sectors and `18` stacks.
169    #[expect(
170        clippy::explicit_counter_loop,
171        reason = "Clippy suggestion was much less clear."
172    )]
173    pub fn uv(&self, sectors: u32, stacks: u32) -> Mesh {
174        // Largely inspired from http://www.songho.ca/opengl/gl_sphere.html
175
176        let sectors_f32 = sectors as f32;
177        let stacks_f32 = stacks as f32;
178        let length_inv = 1. / self.sphere.radius;
179        let sector_step = 2. * PI / sectors_f32;
180        let stack_step = PI / stacks_f32;
181
182        let n_vertices = (stacks * sectors) as usize;
183        let mut vertices: Vec<[f32; 3]> = Vec::with_capacity(n_vertices);
184        let mut normals: Vec<[f32; 3]> = Vec::with_capacity(n_vertices);
185        let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(n_vertices);
186        let mut indices: Vec<u32> = Vec::with_capacity(n_vertices * 2 * 3);
187
188        for i in 0..stacks + 1 {
189            let stack_angle = PI / 2. - (i as f32) * stack_step;
190            let xy = self.sphere.radius * ops::cos(stack_angle);
191            let z = self.sphere.radius * ops::sin(stack_angle);
192
193            for j in 0..sectors + 1 {
194                let sector_angle = (j as f32) * sector_step;
195                let x = xy * ops::cos(sector_angle);
196                let y = xy * ops::sin(sector_angle);
197
198                vertices.push([x, y, z]);
199                normals.push([x * length_inv, y * length_inv, z * length_inv]);
200                uvs.push([(j as f32) / sectors_f32, (i as f32) / stacks_f32]);
201            }
202        }
203
204        // indices
205        //  k1--k1+1
206        //  |  / |
207        //  | /  |
208        //  k2--k2+1
209        for i in 0..stacks {
210            let mut k1 = i * (sectors + 1);
211            let mut k2 = k1 + sectors + 1;
212            for _j in 0..sectors {
213                if i != 0 {
214                    indices.push(k1);
215                    indices.push(k2);
216                    indices.push(k1 + 1);
217                }
218                if i != stacks - 1 {
219                    indices.push(k1 + 1);
220                    indices.push(k2);
221                    indices.push(k2 + 1);
222                }
223                k1 += 1;
224                k2 += 1;
225            }
226        }
227
228        Mesh::new(
229            PrimitiveTopology::TriangleList,
230            RenderAssetUsages::default(),
231        )
232        .with_inserted_indices(Indices::U32(indices))
233        .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
234        .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
235        .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
236    }
237}
238
239impl MeshBuilder for SphereMeshBuilder {
240    /// Builds a [`Mesh`] according to the configuration in `self`.
241    ///
242    /// # Panics
243    ///
244    /// Panics if the sphere is a [`SphereKind::Ico`] with a subdivision count
245    /// that is greater than or equal to `80` because there will be too many vertices.
246    fn build(&self) -> Mesh {
247        match self.kind {
248            SphereKind::Ico { subdivisions } => self.ico(subdivisions).unwrap(),
249            SphereKind::Uv { sectors, stacks } => self.uv(sectors, stacks),
250        }
251    }
252}
253
254impl Meshable for Sphere {
255    type Output = SphereMeshBuilder;
256
257    fn mesh(&self) -> Self::Output {
258        SphereMeshBuilder {
259            sphere: *self,
260            ..Default::default()
261        }
262    }
263}
264
265impl From<Sphere> for Mesh {
266    fn from(sphere: Sphere) -> Self {
267        sphere.mesh().build()
268    }
269}