1use crate::resources::MeshData;
2
3pub fn cube(size: f32) -> MeshData {
7 let h = size / 2.0;
8
9 #[rustfmt::skip]
11 let positions: Vec<[f32; 3]> = vec![
12 [-h, -h, h], [ h, -h, h], [ h, h, h], [-h, h, h],
14 [ h, -h, -h], [-h, -h, -h], [-h, h, -h], [ h, h, -h],
16 [-h, h, h], [ h, h, h], [ h, h, -h], [-h, h, -h],
18 [-h, -h, -h], [ h, -h, -h], [ h, -h, h], [-h, -h, h],
20 [ h, -h, h], [ h, -h, -h], [ h, h, -h], [ h, h, h],
22 [-h, -h, -h], [-h, -h, h], [-h, h, h], [-h, h, -h],
24 ];
25
26 let face_normals: [[f32; 3]; 6] = [
28 [ 0.0, 0.0, 1.0],
29 [ 0.0, 0.0, -1.0],
30 [ 0.0, 1.0, 0.0],
31 [ 0.0, -1.0, 0.0],
32 [ 1.0, 0.0, 0.0],
33 [-1.0, 0.0, 0.0],
34 ];
35 let normals: Vec<[f32; 3]> = face_normals
36 .iter()
37 .flat_map(|n| std::iter::repeat(*n).take(4))
38 .collect();
39
40 let indices: Vec<u32> = (0..6u32)
42 .flat_map(|f| {
43 let b = f * 4;
44 [b, b + 1, b + 2, b, b + 2, b + 3]
45 })
46 .collect();
47
48 MeshData { positions, normals, indices, ..MeshData::default() }
49}
50
51pub fn sphere(radius: f32, sectors: u32, stacks: u32) -> MeshData {
57 let sectors = sectors.max(3);
58 let stacks = stacks.max(2);
59
60 let mut positions: Vec<[f32; 3]> = Vec::new();
61 let mut normals: Vec<[f32; 3]> = Vec::new();
62 let mut indices: Vec<u32> = Vec::new();
63
64 let sector_step = 2.0 * std::f32::consts::PI / sectors as f32;
65 let stack_step = std::f32::consts::PI / stacks as f32;
66
67 for i in 0..=stacks {
68 let stack_angle = std::f32::consts::FRAC_PI_2 - i as f32 * stack_step;
69 let xy = radius * stack_angle.cos();
70 let z = radius * stack_angle.sin();
71
72 for j in 0..=sectors {
73 let sector_angle = j as f32 * sector_step;
74 let x = xy * sector_angle.cos();
75 let y = xy * sector_angle.sin();
76 positions.push([x, y, z]);
77 normals.push([x / radius, y / radius, z / radius]);
78 }
79 }
80
81 for i in 0..stacks {
82 let k1 = i * (sectors + 1);
83 let k2 = k1 + sectors + 1;
84 for j in 0..sectors {
85 if i != 0 {
86 indices.push(k1 + j);
87 indices.push(k2 + j);
88 indices.push(k1 + j + 1);
89 }
90 if i != stacks - 1 {
91 indices.push(k1 + j + 1);
92 indices.push(k2 + j);
93 indices.push(k2 + j + 1);
94 }
95 }
96 }
97
98 MeshData { positions, normals, indices, ..MeshData::default() }
99}
100
101pub fn plane(width: f32, depth: f32) -> MeshData {
105 let hw = width / 2.0;
106 let hd = depth / 2.0;
107
108 let positions = vec![
109 [-hw, 0.0, -hd],
110 [ hw, 0.0, -hd],
111 [ hw, 0.0, hd],
112 [-hw, 0.0, hd],
113 ];
114 let normals = vec![[0.0, 1.0, 0.0]; 4];
115 let indices = vec![0, 2, 1, 0, 3, 2];
116
117 MeshData { positions, normals, indices, ..MeshData::default() }
118}
119
120pub fn cylinder(radius: f32, height: f32, sectors: u32) -> MeshData {
124 let sectors = sectors.max(3);
125 let half_h = height / 2.0;
126 let step = 2.0 * std::f32::consts::PI / sectors as f32;
127
128 let mut positions: Vec<[f32; 3]> = Vec::new();
129 let mut normals: Vec<[f32; 3]> = Vec::new();
130 let mut indices: Vec<u32> = Vec::new();
131
132 for &y in &[-half_h, half_h] {
134 for j in 0..sectors {
135 let angle = j as f32 * step;
136 let x = radius * angle.cos();
137 let z = radius * angle.sin();
138 positions.push([x, y, z]);
139 normals.push([angle.cos(), 0.0, angle.sin()]);
140 }
141 }
142
143 for j in 0..sectors {
145 let b = j;
146 let next = (j + 1) % sectors;
147 let t = j + sectors;
148 let t_next = next + sectors;
149 indices.extend_from_slice(&[b, t_next, next, b, t, t_next]);
150 }
151
152 let bottom_center = positions.len() as u32;
154 positions.push([0.0, -half_h, 0.0]);
155 normals.push([0.0, -1.0, 0.0]);
156
157 let top_center = positions.len() as u32;
158 positions.push([0.0, half_h, 0.0]);
159 normals.push([0.0, 1.0, 0.0]);
160
161 let bottom_rim_start = positions.len() as u32;
163 for j in 0..sectors {
164 let angle = j as f32 * step;
165 positions.push([radius * angle.cos(), -half_h, radius * angle.sin()]);
166 normals.push([0.0, -1.0, 0.0]);
167 }
168
169 let top_rim_start = positions.len() as u32;
170 for j in 0..sectors {
171 let angle = j as f32 * step;
172 positions.push([radius * angle.cos(), half_h, radius * angle.sin()]);
173 normals.push([0.0, 1.0, 0.0]);
174 }
175
176 for j in 0..sectors as u32 {
178 let next = (j + 1) % sectors as u32;
179 indices.extend_from_slice(&[bottom_center, bottom_rim_start + j, bottom_rim_start + next]);
181 indices.extend_from_slice(&[top_center, top_rim_start + next, top_rim_start + j]);
183 }
184
185 MeshData { positions, normals, indices, ..MeshData::default() }
186}
187
188pub fn cuboid(width: f32, height: f32, depth: f32) -> MeshData {
192 let hw = width / 2.0;
193 let hh = height / 2.0;
194 let hd = depth / 2.0;
195
196 #[rustfmt::skip]
197 let positions: Vec<[f32; 3]> = vec![
198 [-hw, -hh, hd], [ hw, -hh, hd], [ hw, hh, hd], [-hw, hh, hd],
200 [ hw, -hh, -hd], [-hw, -hh, -hd], [-hw, hh, -hd], [ hw, hh, -hd],
202 [-hw, hh, hd], [ hw, hh, hd], [ hw, hh, -hd], [-hw, hh, -hd],
204 [-hw, -hh, -hd], [ hw, -hh, -hd], [ hw, -hh, hd], [-hw, -hh, hd],
206 [ hw, -hh, hd], [ hw, -hh, -hd], [ hw, hh, -hd], [ hw, hh, hd],
208 [-hw, -hh, -hd], [-hw, -hh, hd], [-hw, hh, hd], [-hw, hh, -hd],
210 ];
211
212 let face_normals: [[f32; 3]; 6] = [
213 [ 0.0, 0.0, 1.0],
214 [ 0.0, 0.0, -1.0],
215 [ 0.0, 1.0, 0.0],
216 [ 0.0, -1.0, 0.0],
217 [ 1.0, 0.0, 0.0],
218 [-1.0, 0.0, 0.0],
219 ];
220 let normals: Vec<[f32; 3]> = face_normals
221 .iter()
222 .flat_map(|n| std::iter::repeat(*n).take(4))
223 .collect();
224
225 let indices: Vec<u32> = (0..6u32)
226 .flat_map(|f| {
227 let b = f * 4;
228 [b, b + 1, b + 2, b, b + 2, b + 3]
229 })
230 .collect();
231
232 MeshData { positions, normals, indices, ..MeshData::default() }
233}
234
235pub fn cone(radius: f32, height: f32, sectors: u32) -> MeshData {
239 let sectors = sectors.max(3);
240 let half_h = height / 2.0;
241 let step = 2.0 * std::f32::consts::PI / sectors as f32;
242
243 let hyp = (radius * radius + height * height).sqrt();
245 let ny = radius / hyp;
246 let nr = height / hyp;
247
248 let mut positions: Vec<[f32; 3]> = Vec::new();
249 let mut normals: Vec<[f32; 3]> = Vec::new();
250 let mut indices: Vec<u32> = Vec::new();
251
252 for j in 0..sectors {
254 let a0 = j as f32 * step;
255 let a1 = (j + 1) as f32 * step;
256 let amid = (a0 + a1) * 0.5;
257 let base = positions.len() as u32;
258
259 positions.push([0.0, half_h, 0.0]);
260 normals.push([nr * amid.cos(), ny, nr * amid.sin()]);
261
262 positions.push([radius * a0.cos(), -half_h, radius * a0.sin()]);
263 normals.push([nr * a0.cos(), ny, nr * a0.sin()]);
264
265 positions.push([radius * a1.cos(), -half_h, radius * a1.sin()]);
266 normals.push([nr * a1.cos(), ny, nr * a1.sin()]);
267
268 indices.extend_from_slice(&[base, base + 2, base + 1]);
269 }
270
271 let bottom_center = positions.len() as u32;
273 positions.push([0.0, -half_h, 0.0]);
274 normals.push([0.0, -1.0, 0.0]);
275
276 let rim_start = positions.len() as u32;
277 for j in 0..sectors {
278 let a = j as f32 * step;
279 positions.push([radius * a.cos(), -half_h, radius * a.sin()]);
280 normals.push([0.0, -1.0, 0.0]);
281 }
282 for j in 0..sectors as u32 {
283 let next = (j + 1) % sectors as u32;
284 indices.extend_from_slice(&[bottom_center, rim_start + j, rim_start + next]);
285 }
286
287 MeshData { positions, normals, indices, ..MeshData::default() }
288}
289
290pub fn capsule(radius: f32, height: f32, sectors: u32, stacks: u32) -> MeshData {
295 let sectors = sectors.max(3);
296 let stacks = stacks.max(2);
297 let body_height = (height - 2.0 * radius).max(0.0);
298 let half_body = body_height / 2.0;
299 let hemi_stacks = (stacks / 2).max(1);
300 let cols = sectors + 1;
301
302 let mut positions: Vec<[f32; 3]> = Vec::new();
303 let mut normals: Vec<[f32; 3]> = Vec::new();
304 let mut indices: Vec<u32> = Vec::new();
305
306 for i in 0..=hemi_stacks {
308 let phi = std::f32::consts::FRAC_PI_2 * (1.0 - i as f32 / hemi_stacks as f32);
309 let sin_phi = phi.sin();
310 let cos_phi = phi.cos();
311 for j in 0..=sectors {
312 let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
313 let nx = cos_phi * theta.cos();
314 let nz = cos_phi * theta.sin();
315 positions.push([radius * nx, half_body + radius * sin_phi, radius * nz]);
316 normals.push([nx, sin_phi, nz]);
317 }
318 }
319
320 let bottom_off = (hemi_stacks + 1) as u32;
322 for i in 0..=hemi_stacks {
323 let phi = -std::f32::consts::FRAC_PI_2 * i as f32 / hemi_stacks as f32;
324 let sin_phi = phi.sin();
325 let cos_phi = phi.cos();
326 for j in 0..=sectors {
327 let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
328 let nx = cos_phi * theta.cos();
329 let nz = cos_phi * theta.sin();
330 positions.push([radius * nx, -half_body + radius * sin_phi, radius * nz]);
331 normals.push([nx, sin_phi, nz]);
332 }
333 }
334
335 for i in 0..hemi_stacks {
337 let k1 = i * cols;
338 let k2 = k1 + cols;
339 for j in 0..sectors {
340 if i != 0 {
341 indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
342 }
343 indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
344 }
345 }
346
347 if body_height > 1e-6 {
349 let k1 = hemi_stacks * cols;
350 let k2 = bottom_off * cols;
351 for j in 0..sectors {
352 indices.extend_from_slice(&[
353 k1 + j, k1 + j + 1, k2 + j,
354 k1 + j + 1, k2 + j + 1, k2 + j,
355 ]);
356 }
357 }
358
359 for i in 0..hemi_stacks {
361 let k1 = (bottom_off + i) * cols;
362 let k2 = k1 + cols;
363 for j in 0..sectors {
364 indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
365 if i != hemi_stacks - 1 {
366 indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
367 }
368 }
369 }
370
371 MeshData { positions, normals, indices, ..MeshData::default() }
372}
373
374pub fn torus(major_radius: f32, minor_radius: f32, sectors: u32, stacks: u32) -> MeshData {
381 let sectors = sectors.max(3);
382 let stacks = stacks.max(3);
383
384 let mut positions: Vec<[f32; 3]> = Vec::new();
385 let mut normals: Vec<[f32; 3]> = Vec::new();
386 let mut indices: Vec<u32> = Vec::new();
387
388 for i in 0..=stacks {
389 let phi = i as f32 * std::f32::consts::TAU / stacks as f32;
390 let cos_phi = phi.cos();
391 let sin_phi = phi.sin();
392 let cx = major_radius * cos_phi;
393 let cz = major_radius * sin_phi;
394
395 for j in 0..=sectors {
396 let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
397 let cos_theta = theta.cos();
398 let sin_theta = theta.sin();
399
400 let nx = cos_phi * cos_theta;
401 let ny = sin_theta;
402 let nz = sin_phi * cos_theta;
403
404 positions.push([cx + minor_radius * nx, minor_radius * ny, cz + minor_radius * nz]);
405 normals.push([nx, ny, nz]);
406 }
407 }
408
409 let cols = sectors + 1;
410 for i in 0..stacks {
411 let k1 = i * cols;
412 let k2 = k1 + cols;
413 for j in 0..sectors {
414 indices.extend_from_slice(&[
415 k1 + j, k1 + j + 1, k2 + j,
416 k1 + j + 1, k2 + j + 1, k2 + j,
417 ]);
418 }
419 }
420
421 MeshData { positions, normals, indices, ..MeshData::default() }
422}
423
424pub fn icosphere(radius: f32, subdivisions: u32) -> MeshData {
428 let phi = (1.0 + 5.0f32.sqrt()) / 2.0;
429 let norm = (1.0 + phi * phi).sqrt();
430 let a = 1.0 / norm;
431 let b = phi / norm;
432
433 let mut verts: Vec<[f32; 3]> = vec![
434 [-a, b, 0.0], [ a, b, 0.0], [-a, -b, 0.0], [ a, -b, 0.0],
435 [ 0.0, -a, b], [ 0.0, a, b], [ 0.0, -a, -b], [ 0.0, a, -b],
436 [ b, 0.0, -a], [ b, 0.0, a], [-b, 0.0, -a], [-b, 0.0, a],
437 ];
438 let mut faces: Vec<[u32; 3]> = vec![
439 [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
440 [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
441 [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
442 [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1],
443 ];
444
445 for _ in 0..subdivisions {
446 let mut new_faces: Vec<[u32; 3]> = Vec::with_capacity(faces.len() * 4);
447 let mut cache: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
448 for &[va, vb, vc] in &faces {
449 let mab = ico_midpoint(&mut verts, &mut cache, va, vb);
450 let mbc = ico_midpoint(&mut verts, &mut cache, vb, vc);
451 let mca = ico_midpoint(&mut verts, &mut cache, vc, va);
452 new_faces.push([va, mab, mca]);
453 new_faces.push([vb, mbc, mab]);
454 new_faces.push([vc, mca, mbc]);
455 new_faces.push([mab, mbc, mca]);
456 }
457 faces = new_faces;
458 }
459
460 let normals: Vec<[f32; 3]> = verts.clone();
461 let positions: Vec<[f32; 3]> = verts.iter().map(|v| [v[0] * radius, v[1] * radius, v[2] * radius]).collect();
462 let indices: Vec<u32> = faces.iter().flat_map(|f| f.iter().copied()).collect();
463
464 MeshData { positions, normals, indices, ..MeshData::default() }
465}
466
467fn ico_midpoint(
468 verts: &mut Vec<[f32; 3]>,
469 cache: &mut std::collections::HashMap<u64, u32>,
470 a: u32,
471 b: u32,
472) -> u32 {
473 let key = if a < b { (a as u64) << 32 | b as u64 } else { (b as u64) << 32 | a as u64 };
474 if let Some(&idx) = cache.get(&key) {
475 return idx;
476 }
477 let va = verts[a as usize];
478 let vb = verts[b as usize];
479 let mx = (va[0] + vb[0]) * 0.5;
480 let my = (va[1] + vb[1]) * 0.5;
481 let mz = (va[2] + vb[2]) * 0.5;
482 let len = (mx * mx + my * my + mz * mz).sqrt();
483 let idx = verts.len() as u32;
484 verts.push([mx / len, my / len, mz / len]);
485 cache.insert(key, idx);
486 idx
487}
488
489pub fn arrow(shaft_radius: f32, head_radius: f32, head_fraction: f32, sectors: u32) -> MeshData {
496 let sectors = sectors.max(3);
497 let head_fraction = head_fraction.clamp(0.1, 0.9);
498 let step = std::f32::consts::TAU / sectors as f32;
499
500 let shaft_bot: f32 = -0.5;
501 let shaft_top: f32 = 0.5 - head_fraction;
502 let head_bot: f32 = shaft_top;
503 let head_top: f32 = 0.5;
504 let head_h = head_fraction;
505
506 let mut positions: Vec<[f32; 3]> = Vec::new();
507 let mut normals: Vec<[f32; 3]> = Vec::new();
508 let mut indices: Vec<u32> = Vec::new();
509
510 for &y in &[shaft_bot, shaft_top] {
512 for j in 0..sectors {
513 let a = j as f32 * step;
514 positions.push([shaft_radius * a.cos(), y, shaft_radius * a.sin()]);
515 normals.push([a.cos(), 0.0, a.sin()]);
516 }
517 }
518 for j in 0..sectors {
519 let next = (j + 1) % sectors;
520 let t = j + sectors;
521 let t_next = next + sectors;
522 indices.extend_from_slice(&[j, t_next, next, j, t, t_next]);
523 }
524
525 let sb_center = positions.len() as u32;
527 positions.push([0.0, shaft_bot, 0.0]);
528 normals.push([0.0, -1.0, 0.0]);
529 let sb_rim = positions.len() as u32;
530 for j in 0..sectors {
531 let a = j as f32 * step;
532 positions.push([shaft_radius * a.cos(), shaft_bot, shaft_radius * a.sin()]);
533 normals.push([0.0, -1.0, 0.0]);
534 }
535 for j in 0..sectors as u32 {
536 let next = (j + 1) % sectors as u32;
537 indices.extend_from_slice(&[sb_center, sb_rim + j, sb_rim + next]);
538 }
539
540 let cone_hyp = (head_radius * head_radius + head_h * head_h).sqrt();
542 let cny = head_radius / cone_hyp;
543 let cnr = head_h / cone_hyp;
544 for j in 0..sectors {
545 let a0 = j as f32 * step;
546 let a1 = (j + 1) as f32 * step;
547 let amid = (a0 + a1) * 0.5;
548 let base = positions.len() as u32;
549
550 positions.push([0.0, head_top, 0.0]);
551 normals.push([cnr * amid.cos(), cny, cnr * amid.sin()]);
552
553 positions.push([head_radius * a0.cos(), head_bot, head_radius * a0.sin()]);
554 normals.push([cnr * a0.cos(), cny, cnr * a0.sin()]);
555
556 positions.push([head_radius * a1.cos(), head_bot, head_radius * a1.sin()]);
557 normals.push([cnr * a1.cos(), cny, cnr * a1.sin()]);
558
559 indices.extend_from_slice(&[base, base + 2, base + 1]);
560 }
561
562 let hb_center = positions.len() as u32;
564 positions.push([0.0, head_bot, 0.0]);
565 normals.push([0.0, -1.0, 0.0]);
566 let hb_rim = positions.len() as u32;
567 for j in 0..sectors {
568 let a = j as f32 * step;
569 positions.push([head_radius * a.cos(), head_bot, head_radius * a.sin()]);
570 normals.push([0.0, -1.0, 0.0]);
571 }
572 for j in 0..sectors as u32 {
573 let next = (j + 1) % sectors as u32;
574 indices.extend_from_slice(&[hb_center, hb_rim + j, hb_rim + next]);
575 }
576
577 MeshData { positions, normals, indices, ..MeshData::default() }
578}
579
580pub fn disk(radius: f32, sectors: u32) -> MeshData {
584 let sectors = sectors.max(3);
585 let step = std::f32::consts::TAU / sectors as f32;
586
587 let mut positions: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0]];
588 let mut normals: Vec<[f32; 3]> = vec![[0.0, 1.0, 0.0]];
589 let mut indices: Vec<u32> = Vec::new();
590
591 for j in 0..sectors {
592 let a = j as f32 * step;
593 positions.push([radius * a.cos(), 0.0, radius * a.sin()]);
594 normals.push([0.0, 1.0, 0.0]);
595 }
596
597 for j in 0..sectors as u32 {
598 let next = (j + 1) % sectors as u32;
599 indices.extend_from_slice(&[0, next + 1, j + 1]);
600 }
601
602 MeshData { positions, normals, indices, ..MeshData::default() }
603}
604
605pub fn frustum(fov_y: f32, aspect: f32, near: f32, far: f32) -> MeshData {
611 let half_h_n = near * (fov_y * 0.5).tan();
612 let half_w_n = half_h_n * aspect;
613 let half_h_f = far * (fov_y * 0.5).tan();
614 let half_w_f = half_h_f * aspect;
615
616 let nbl = [-half_w_n, -half_h_n, -near];
618 let nbr = [ half_w_n, -half_h_n, -near];
619 let ntr = [ half_w_n, half_h_n, -near];
620 let ntl = [-half_w_n, half_h_n, -near];
621 let fbl = [-half_w_f, -half_h_f, -far];
622 let fbr = [ half_w_f, -half_h_f, -far];
623 let ftr = [ half_w_f, half_h_f, -far];
624 let ftl = [-half_w_f, half_h_f, -far];
625
626 let face_quads: [[[f32; 3]; 4]; 6] = [
628 [ntl, ntr, nbr, nbl], [fbl, fbr, ftr, ftl], [ntl, ftl, ftr, ntr], [nbr, fbr, fbl, nbl], [ntr, ftr, fbr, nbr], [nbl, fbl, ftl, ntl], ];
635
636 let mut positions: Vec<[f32; 3]> = Vec::new();
637 let mut normals: Vec<[f32; 3]> = Vec::new();
638 let mut indices: Vec<u32> = Vec::new();
639
640 for quad in &face_quads {
641 let [v0, v1, _, v3] = quad;
642 let e1 = [v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]];
643 let e2 = [v3[0]-v0[0], v3[1]-v0[1], v3[2]-v0[2]];
644 let nr = [
645 e1[1]*e2[2] - e1[2]*e2[1],
646 e1[2]*e2[0] - e1[0]*e2[2],
647 e1[0]*e2[1] - e1[1]*e2[0],
648 ];
649 let len = (nr[0]*nr[0] + nr[1]*nr[1] + nr[2]*nr[2]).sqrt();
650 let n = if len > 0.0 { [nr[0]/len, nr[1]/len, nr[2]/len] } else { [0.0, 0.0, 1.0] };
651
652 let base = positions.len() as u32;
653 for v in quad {
654 positions.push(*v);
655 normals.push(n);
656 }
657 indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
658 }
659
660 MeshData { positions, normals, indices, ..MeshData::default() }
661}
662
663pub fn hemisphere(radius: f32, sectors: u32, stacks: u32) -> MeshData {
668 let sectors = sectors.max(3);
669 let stacks = stacks.max(1);
670
671 let mut positions: Vec<[f32; 3]> = Vec::new();
672 let mut normals: Vec<[f32; 3]> = Vec::new();
673 let mut indices: Vec<u32> = Vec::new();
674
675 for i in 0..=stacks {
676 let phi = std::f32::consts::FRAC_PI_2 * (1.0 - i as f32 / stacks as f32);
677 let sin_phi = phi.sin();
678 let cos_phi = phi.cos();
679 for j in 0..=sectors {
680 let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
681 let nx = cos_phi * theta.cos();
682 let nz = cos_phi * theta.sin();
683 positions.push([radius * nx, radius * sin_phi, radius * nz]);
684 normals.push([nx, sin_phi, nz]);
685 }
686 }
687
688 let cols = sectors + 1;
689 for i in 0..stacks {
690 let k1 = i * cols;
691 let k2 = k1 + cols;
692 for j in 0..sectors {
693 if i != 0 {
694 indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
695 }
696 indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
697 }
698 }
699
700 let center = positions.len() as u32;
702 positions.push([0.0, 0.0, 0.0]);
703 normals.push([0.0, -1.0, 0.0]);
704 let rim_start = positions.len() as u32;
705 for j in 0..sectors {
706 let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
707 positions.push([radius * theta.cos(), 0.0, radius * theta.sin()]);
708 normals.push([0.0, -1.0, 0.0]);
709 }
710 for j in 0..sectors as u32 {
711 let next = (j + 1) % sectors as u32;
712 indices.extend_from_slice(&[center, rim_start + j, rim_start + next]);
713 }
714
715 MeshData { positions, normals, indices, ..MeshData::default() }
716}
717
718pub fn ring(inner_radius: f32, outer_radius: f32, sectors: u32) -> MeshData {
722 let sectors = sectors.max(3);
723 let step = std::f32::consts::TAU / sectors as f32;
724
725 let mut positions: Vec<[f32; 3]> = Vec::new();
726 let mut normals: Vec<[f32; 3]> = Vec::new();
727 let mut indices: Vec<u32> = Vec::new();
728
729 for j in 0..=sectors {
731 let a = j as f32 * step;
732 let cos_a = a.cos();
733 let sin_a = a.sin();
734 positions.push([inner_radius * cos_a, 0.0, inner_radius * sin_a]);
735 normals.push([0.0, 1.0, 0.0]);
736 positions.push([outer_radius * cos_a, 0.0, outer_radius * sin_a]);
737 normals.push([0.0, 1.0, 0.0]);
738 }
739
740 for j in 0..sectors as u32 {
741 let i0 = j * 2;
742 let o0 = i0 + 1;
743 let i1 = i0 + 2;
744 let o1 = i0 + 3;
745 indices.extend_from_slice(&[i0, i1, o0, i1, o1, o0]);
746 }
747
748 MeshData { positions, normals, indices, ..MeshData::default() }
749}
750
751pub fn ellipsoid(rx: f32, ry: f32, rz: f32, sectors: u32, stacks: u32) -> MeshData {
756 let sectors = sectors.max(3);
757 let stacks = stacks.max(2);
758
759 let mut positions: Vec<[f32; 3]> = Vec::new();
760 let mut normals: Vec<[f32; 3]> = Vec::new();
761 let mut indices: Vec<u32> = Vec::new();
762
763 let sector_step = std::f32::consts::TAU / sectors as f32;
764 let stack_step = std::f32::consts::PI / stacks as f32;
765
766 for i in 0..=stacks {
767 let stack_angle = std::f32::consts::FRAC_PI_2 - i as f32 * stack_step;
768 let cos_sa = stack_angle.cos();
769 let sin_sa = stack_angle.sin();
770
771 for j in 0..=sectors {
772 let sector_angle = j as f32 * sector_step;
773 let cos_se = sector_angle.cos();
774 let sin_se = sector_angle.sin();
775
776 let x = rx * cos_sa * cos_se;
777 let y = ry * sin_sa;
778 let z = rz * cos_sa * sin_se;
779 positions.push([x, y, z]);
780
781 let nx = x / (rx * rx);
783 let ny = y / (ry * ry);
784 let nz = z / (rz * rz);
785 let len = (nx*nx + ny*ny + nz*nz).sqrt();
786 normals.push(if len > 0.0 { [nx/len, ny/len, nz/len] } else { [0.0, 1.0, 0.0] });
787 }
788 }
789
790 for i in 0..stacks {
791 let k1 = i * (sectors + 1);
792 let k2 = k1 + sectors + 1;
793 for j in 0..sectors {
794 if i != 0 {
795 indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
796 }
797 if i != stacks - 1 {
798 indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
799 }
800 }
801 }
802
803 MeshData { positions, normals, indices, ..MeshData::default() }
804}
805
806pub fn spring(radius: f32, coil_radius: f32, turns: f32, sectors: u32) -> MeshData {
813 let sectors = sectors.max(3);
814 let pitch = 2.5 * coil_radius; let height = turns * pitch;
816 let n_segs = (turns * 16.0).ceil() as u32;
817 let total_t = std::f32::consts::TAU * turns;
818
819 let mut positions: Vec<[f32; 3]> = Vec::new();
820 let mut normals: Vec<[f32; 3]> = Vec::new();
821 let mut indices: Vec<u32> = Vec::new();
822
823 for seg in 0..=n_segs {
824 let t = seg as f32 / n_segs as f32 * total_t;
825
826 let cx = radius * t.cos();
827 let cy = t / std::f32::consts::TAU * pitch - height * 0.5;
828 let cz = radius * t.sin();
829
830 let dtx = -radius * t.sin();
832 let dty = pitch / std::f32::consts::TAU;
833 let dtz = radius * t.cos();
834 let dt_len = (dtx*dtx + dty*dty + dtz*dtz).sqrt();
835 let (tx, ty, tz) = (dtx / dt_len, dty / dt_len, dtz / dt_len);
836
837 let pnx = -t.cos();
839 let pny = 0.0f32;
840 let pnz = -t.sin();
841
842 let bx = ty * pnz - tz * pny;
844 let by = tz * pnx - tx * pnz;
845 let bz = tx * pny - ty * pnx;
846
847 for sec in 0..=sectors {
848 let phi = sec as f32 * std::f32::consts::TAU / sectors as f32;
849 let cp = phi.cos();
850 let sp = phi.sin();
851
852 let on_x = cp * pnx + sp * bx;
853 let on_y = cp * pny + sp * by;
854 let on_z = cp * pnz + sp * bz;
855
856 positions.push([cx + coil_radius * on_x, cy + coil_radius * on_y, cz + coil_radius * on_z]);
857 normals.push([on_x, on_y, on_z]);
858 }
859 }
860
861 let cols = sectors + 1;
862 for seg in 0..n_segs {
863 let k1 = seg * cols;
864 let k2 = k1 + cols;
865 for sec in 0..sectors {
866 indices.extend_from_slice(&[
867 k1 + sec, k1 + sec + 1, k2 + sec,
868 k1 + sec + 1, k2 + sec + 1, k2 + sec,
869 ]);
870 }
871 }
872
873 MeshData { positions, normals, indices, ..MeshData::default() }
874}
875
876pub fn grid_plane(width: f32, depth: f32, cols: u32, rows: u32) -> MeshData {
881 let cols = cols.max(1);
882 let rows = rows.max(1);
883 let hw = width * 0.5;
884 let hd = depth * 0.5;
885
886 let mut positions: Vec<[f32; 3]> = Vec::new();
887 let mut normals: Vec<[f32; 3]> = Vec::new();
888 let mut indices: Vec<u32> = Vec::new();
889
890 for row in 0..=rows {
891 let z = -hd + row as f32 / rows as f32 * depth;
892 for col in 0..=cols {
893 let x = -hw + col as f32 / cols as f32 * width;
894 positions.push([x, 0.0, z]);
895 normals.push([0.0, 1.0, 0.0]);
896 }
897 }
898
899 let v_cols = cols + 1;
900 for row in 0..rows {
901 for col in 0..cols {
902 let tl = row * v_cols + col;
903 let tr = tl + 1;
904 let bl = tl + v_cols;
905 let br = bl + 1;
906 indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
907 }
908 }
909
910 MeshData { positions, normals, indices, ..MeshData::default() }
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916
917 fn assert_triangle_winding_matches_normals(mesh: &MeshData) {
918 for tri in mesh.indices.chunks_exact(3) {
919 let ia = tri[0] as usize;
920 let ib = tri[1] as usize;
921 let ic = tri[2] as usize;
922
923 let a = glam::Vec3::from_array(mesh.positions[ia]);
924 let b = glam::Vec3::from_array(mesh.positions[ib]);
925 let c = glam::Vec3::from_array(mesh.positions[ic]);
926 let face_normal = (b - a).cross(c - a);
927 if face_normal.length_squared() <= 1e-12 {
928 continue;
929 }
930
931 let avg_vertex_normal =
932 glam::Vec3::from_array(mesh.normals[ia])
933 + glam::Vec3::from_array(mesh.normals[ib])
934 + glam::Vec3::from_array(mesh.normals[ic]);
935
936 assert!(
937 face_normal.dot(avg_vertex_normal) > 0.0,
938 "triangle winding does not match vertex normals: {tri:?}"
939 );
940 }
941 }
942
943 #[test]
944 fn generated_primitives_have_consistent_outward_winding() {
945 let meshes = [
946 ("cube", cube(1.0)),
947 ("sphere", sphere(1.0, 24, 12)),
948 ("plane", plane(1.0, 1.0)),
949 ("cylinder", cylinder(1.0, 2.0, 24)),
950 ("cuboid", cuboid(1.0, 1.5, 2.0)),
951 ("cone", cone(1.0, 2.0, 24)),
952 ("capsule", capsule(1.0, 3.0, 24, 12)),
953 ("torus", torus(2.0, 0.5, 24, 24)),
954 ("icosphere", icosphere(1.0, 2)),
955 ("arrow", arrow(0.2, 0.4, 0.3, 24)),
956 ("disk", disk(1.0, 24)),
957 ("frustum", frustum(std::f32::consts::FRAC_PI_4, 1.5, 0.1, 2.0)),
958 ("hemisphere", hemisphere(1.0, 24, 12)),
959 ("ring", ring(0.5, 1.0, 24)),
960 ("ellipsoid", ellipsoid(1.0, 0.75, 1.25, 24, 12)),
961 ("spring", spring(2.0, 0.25, 3.0, 16)),
962 ("grid_plane", grid_plane(1.0, 1.0, 4, 4)),
963 ];
964
965 for (name, mesh) in &meshes {
966 eprintln!("checking {name}");
967 assert_triangle_winding_matches_normals(mesh);
968 }
969 }
970}