Skip to main content

myth_resources/primitives/
sphere.rs

1use crate::geometry::{Attribute, Geometry};
2use std::f32::consts::PI;
3use wgpu::VertexFormat;
4
5pub struct SphereOptions {
6    pub radius: f32,
7    pub width_segments: u32,
8    pub height_segments: u32,
9}
10
11impl Default for SphereOptions {
12    fn default() -> Self {
13        Self {
14            radius: 1.0,
15            width_segments: 32,
16            height_segments: 16,
17        }
18    }
19}
20
21#[must_use]
22pub fn create_sphere(options: &SphereOptions) -> Geometry {
23    let radius = options.radius;
24    let width_segments = options.width_segments.max(3);
25    let height_segments = options.height_segments.max(2);
26
27    let mut positions = Vec::new();
28    let mut normals = Vec::new();
29    let mut uvs = Vec::new();
30    let mut indices = Vec::new();
31
32    // Generate vertex data
33    for y in 0..=height_segments {
34        let v_ratio = y as f32 / height_segments as f32;
35        // Latitude angle: from 0 to PI (south pole to north pole)
36        let theta = v_ratio * PI;
37
38        // y coordinate (Y-up)
39        let py = -radius * (theta.cos());
40        // Radius of current latitude ring
41        let ring_radius = radius * (theta.sin());
42
43        for x in 0..=width_segments {
44            let u_ratio = x as f32 / width_segments as f32;
45            // Longitude angle: from 0 to 2*PI
46            let phi = u_ratio * 2.0 * PI;
47
48            let px = -ring_radius * phi.cos();
49            let pz = ring_radius * phi.sin();
50
51            positions.push([px, py, pz]);
52
53            // Normal is the normalized position vector
54            let nx = px / radius;
55            let ny = py / radius;
56            let nz = pz / radius;
57            normals.push([nx, ny, nz]);
58
59            // UV
60            uvs.push([u_ratio, 1.0 - v_ratio]);
61        }
62    }
63
64    // Generate indices
65    // Each grid cell consists of two triangles
66    let stride = width_segments + 1;
67    for y in 0..height_segments {
68        for x in 0..width_segments {
69            let v0 = y * stride + x;
70            let v1 = v0 + 1;
71            let v2 = (y + 1) * stride + x;
72            let v3 = v2 + 1;
73
74            // If not the last row (south pole doesn't need the first triangle, but for simplicity we usually don't do special culling, degenerate triangles will be ignored by the GPU)
75            if y != 0 {
76                indices.push(v0 as u16);
77                indices.push(v1 as u16);
78                indices.push(v2 as u16);
79            }
80
81            // If not the first row (north pole)
82            if y != height_segments - 1 {
83                indices.push(v1 as u16);
84                indices.push(v3 as u16);
85                indices.push(v2 as u16);
86            }
87        }
88    }
89
90    let mut geo = Geometry::new();
91    geo.set_attribute(
92        "position",
93        Attribute::new_planar(&positions, VertexFormat::Float32x3),
94    );
95    geo.set_attribute(
96        "normal",
97        Attribute::new_planar(&normals, VertexFormat::Float32x3),
98    );
99    geo.set_attribute("uv", Attribute::new_planar(&uvs, VertexFormat::Float32x2));
100    geo.set_indices(&indices);
101
102    geo.compute_bounding_volume();
103    geo
104}