1use crate::prelude::*;
3
4pub struct Mesh3d {
7 pub mesh: Mesh,
8 cached: std::cell::OnceCell<crate::retained::ResidentMesh>,
9}
10
11impl Mesh3d {
12 pub fn from_mesh(mesh: Mesh) -> Self {
13 Self {
14 mesh,
15 cached: Default::default(),
16 }
17 }
18 pub fn invalidate_gpu(&mut self) {
20 self.cached.take();
21 }
22 pub fn resident(&self) -> &crate::retained::ResidentMesh {
23 self.cached.get_or_init(|| {
24 crate::retained::ResidentMesh::new(&self.mesh).expect("valid retained Mesh3d")
25 })
26 }
27 pub fn draw_transformed(&self, model: Mat4) {
28 crate::draw_resident(self.resident(), model);
29 }
30 pub fn draw_immediate(&self, model: Mat4) {
32 crate::draw_mesh_transformed(&self.mesh, model);
33 }
34
35 pub fn plane(size: Vec2, color: Color, subdivisions: u16) -> Self {
39 assert!(
40 size.is_finite() && size.min_element() > 0.0,
41 "plane size must be finite and positive"
42 );
43 let n = subdivisions.clamp(1, 28);
44 let stride = n + 1;
45 let mut vertices = Vec::with_capacity(stride as usize * stride as usize);
46 let mut indices = Vec::with_capacity(n as usize * n as usize * 6);
47 for z in 0..=n {
48 for x in 0..=n {
49 let uv = vec2(x as f32, z as f32) / n as f32;
50 let position = (uv - Vec2::splat(0.5)) * size;
51 let mut vertex = Vertex::new2(vec3(position.x, 0.0, position.y), uv, color);
52 vertex.normal = Vec3::Y.extend(0.0);
53 vertices.push(vertex);
54 }
55 }
56 for z in 0..n {
57 for x in 0..n {
58 let a = z * stride + x;
59 let b = a + stride;
60 indices.extend([a, b, a + 1, a + 1, b, b + 1]);
61 }
62 }
63 Self {
64 cached: Default::default(),
65 mesh: Mesh {
66 vertices,
67 indices,
68 texture: None,
69 },
70 }
71 }
72
73 pub fn cylinder(radius: f32, height: f32, color: Color, segments: u16) -> Self {
75 Self::frustum(radius, radius, height, color, segments)
76 }
77
78 pub fn cone(radius: f32, height: f32, color: Color, segments: u16) -> Self {
80 Self::frustum(radius, 0.0, height, color, segments)
81 }
82
83 pub fn frustum(bottom: f32, top: f32, height: f32, color: Color, segments: u16) -> Self {
86 assert!(
87 bottom.is_finite()
88 && top.is_finite()
89 && bottom >= 0.0
90 && top >= 0.0
91 && bottom.max(top) > 0.0
92 && height.is_finite()
93 && height > 0.0,
94 "frustum dimensions must be finite with positive height and at least one positive radius"
95 );
96 let segments = segments.clamp(3, 128);
97 let mut vertices = Vec::with_capacity(segments as usize * 12);
98 let mut triangle = |a: Vec3, b: Vec3, c: Vec3| {
99 let cross = (b - a).cross(c - a);
100 if cross.length_squared() < 1e-16 {
101 return;
102 }
103 let normal = cross.normalize().extend(0.0);
104 for position in [a, b, c] {
105 let mut vertex = Vertex::new2(position, Vec2::ZERO, color);
106 vertex.normal = normal;
107 vertices.push(vertex);
108 }
109 };
110 let y = height * 0.5;
111 for i in 0..segments {
112 let angle = |n: u16| n as f32 / segments as f32 * std::f32::consts::TAU;
113 let ring = |theta: f32, radius: f32, y: f32| {
114 vec3(theta.cos() * radius, y, theta.sin() * radius)
115 };
116 let a = ring(angle(i), bottom, -y);
117 let b = ring(angle(i + 1), bottom, -y);
118 let c = ring(angle(i), top, y);
119 let d = ring(angle(i + 1), top, y);
120 triangle(a, c, b);
121 triangle(b, c, d);
122 triangle(vec3(0.0, -y, 0.0), a, b);
123 triangle(vec3(0.0, y, 0.0), d, c);
124 }
125 let indices = (0..vertices.len() as u16).collect();
126 Self {
127 cached: Default::default(),
128 mesh: Mesh {
129 vertices,
130 indices,
131 texture: None,
132 },
133 }
134 }
135
136 pub fn cuboid(size: Vec3, color: Color) -> Self {
137 assert!(
138 size.is_finite() && size.min_element() > 0.0,
139 "cuboid size must be finite and positive"
140 );
141 let mut vertices = Vec::with_capacity(24);
142 let mut indices = Vec::with_capacity(36);
143 let faces = [
144 (Vec3::X, Vec3::Y, Vec3::Z),
145 (-Vec3::X, Vec3::Y, -Vec3::Z),
146 (Vec3::Y, Vec3::Z, Vec3::X),
147 (-Vec3::Y, Vec3::Z, -Vec3::X),
148 (Vec3::Z, Vec3::X, Vec3::Y),
149 (-Vec3::Z, Vec3::X, -Vec3::Y),
150 ];
151 for (normal, u, v) in faces {
152 let base = vertices.len() as u16;
153 for (a, b) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
154 let mut vertex = Vertex::new2(
155 (normal + u * a + v * b) * size * 0.5,
156 vec2((a + 1.0) * 0.5, (b + 1.0) * 0.5),
157 color,
158 );
159 vertex.normal = normal.extend(0.0);
160 vertices.push(vertex);
161 }
162 indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
163 }
164 Self {
165 cached: Default::default(),
166 mesh: Mesh {
167 vertices,
168 indices,
169 texture: None,
170 },
171 }
172 }
173
174 pub fn sphere(radius: f32, color: Color, segments: u16, rings: u16) -> Self {
176 assert!(
177 radius.is_finite() && radius > 0.0,
178 "sphere radius must be finite and positive"
179 );
180 let segments = segments.clamp(3, 64);
181 let rings = rings.clamp(2, 64).min(1 + 4998 / (6 * segments));
184 let mut vertices = Vec::with_capacity((segments as usize + 1) * (rings as usize + 1));
185 let mut indices = Vec::with_capacity(segments as usize * rings as usize * 6);
186 for y in 0..=rings {
187 let v = y as f32 / rings as f32;
188 let phi = v * std::f32::consts::PI;
189 for x in 0..=segments {
190 let u = x as f32 / segments as f32;
191 let theta = u * std::f32::consts::TAU;
192 let normal = if y == 0 {
193 Vec3::Y
194 } else if y == rings {
195 -Vec3::Y
196 } else {
197 vec3(phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin())
198 };
199 let mut vertex = Vertex::new2(normal * radius, vec2(u, v), color);
200 vertex.normal = normal.extend(0.0);
201 vertices.push(vertex);
202 }
203 }
204 for y in 0..rings {
205 for x in 0..segments {
206 let a = y * (segments + 1) + x;
207 let b = a + segments + 1;
208 indices.extend([a, b, a + 1, a + 1, b, b + 1]);
209 }
210 }
211 let mut flat_vertices = Vec::with_capacity(indices.len());
214 for triangle in indices.as_chunks::<3>().0 {
215 let a = vertices[triangle[0] as usize].normal.truncate();
218 let b = vertices[triangle[1] as usize].normal.truncate();
219 let c = vertices[triangle[2] as usize].normal.truncate();
220 let cross = (b - a).cross(c - a);
221 if cross.length_squared() < 1e-12 {
222 continue;
223 }
224 let outward = cross.dot(a + b + c) >= 0.0;
225 let normal = if outward { cross } else { -cross }.normalize();
226 let order = if outward {
228 *triangle
229 } else {
230 [triangle[0], triangle[2], triangle[1]]
231 };
232 for i in order {
233 let mut vertex = vertices[i as usize];
234 vertex.normal = normal.extend(0.0);
235 flat_vertices.push(vertex);
236 }
237 }
238 assert!(
240 flat_vertices.len() < 5000,
241 "sphere tessellation exceeds the low-poly index budget"
242 );
243 let flat_indices = (0..flat_vertices.len() as u16).collect();
244 Self {
245 cached: Default::default(),
246 mesh: Mesh {
247 vertices: flat_vertices,
248 indices: flat_indices,
249 texture: None,
250 },
251 }
252 }
253
254 pub fn draw(&self, position: Vec3, rotation: Quat) {
255 self.draw_transformed(Mat4::from_rotation_translation(rotation, position));
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn maximum_sphere_requests_remain_complete_within_renderer_limits() {
265 for (segments, rings) in [(64, 64), (u16::MAX, u16::MAX), (3, 64), (16, 64)] {
266 let sphere = Mesh3d::sphere(1.0, WHITE, segments, rings);
267 assert!(sphere.mesh.indices.len() < 5000);
268 assert!(sphere.mesh.vertices.len() < 10000);
269 for axis in [Vec3::X, Vec3::Y, Vec3::Z, -Vec3::X, -Vec3::Y, -Vec3::Z] {
270 assert!(
271 sphere
272 .mesh
273 .vertices
274 .iter()
275 .any(|v| v.position.dot(axis) > 0.4)
276 );
277 }
278 }
279 for radius in [0.001, 1.0, 1000.0] {
280 let sphere = Mesh3d::sphere(radius, WHITE, 64, 64);
281 assert_eq!(sphere.mesh.indices.len(), 4992);
282 assert!(sphere.mesh.vertices.iter().all(|v| v.normal.is_finite()));
283 }
284 }
285
286 #[test]
287 fn mesh_indices_and_normals_are_valid() {
288 for shape in [
289 Mesh3d::cuboid(vec3(1.0, 2.0, 3.0), WHITE),
290 Mesh3d::sphere(1.0, WHITE, 16, 12),
291 Mesh3d::cylinder(1.0, 2.0, WHITE, 12),
292 Mesh3d::cone(1.0, 2.0, WHITE, 6),
293 Mesh3d::plane(vec2(100.0, 100.0), WHITE, 20),
294 ] {
295 assert!(
296 shape
297 .mesh
298 .indices
299 .iter()
300 .all(|&i| (i as usize) < shape.mesh.vertices.len())
301 );
302 assert!(
303 shape
304 .mesh
305 .vertices
306 .iter()
307 .all(|v| (v.normal.truncate().length() - 1.0).abs() < 0.0001)
308 );
309 }
310 }
311
312 #[test]
313 fn triangle_winding_matches_outward_flat_normals() {
314 for shape in [
315 Mesh3d::cuboid(vec3(1.0, 2.0, 3.0), WHITE),
316 Mesh3d::sphere(1.0, WHITE, 12, 8),
317 Mesh3d::sphere(0.001, WHITE, 64, 64),
318 Mesh3d::cylinder(1.0, 2.0, WHITE, 12),
319 Mesh3d::cone(1.0, 2.0, WHITE, 6),
320 Mesh3d::frustum(0.0, 1.0, 2.0, WHITE, 6),
321 Mesh3d::plane(vec2(20.0, 10.0), WHITE, 8),
322 ] {
323 for triangle in shape.mesh.indices.as_chunks::<3>().0 {
324 let [a, b, c] = triangle.map(|i| shape.mesh.vertices[i as usize]);
325 let winding = (b.position - a.position).cross(c.position - a.position);
326 assert!(winding.dot(a.normal.truncate()) > 0.0);
327 assert!(a.normal.abs_diff_eq(b.normal, 0.0001));
328 assert!(a.normal.abs_diff_eq(c.normal, 0.0001));
329 }
330 }
331 }
332
333 #[test]
334 fn cylinder_and_cone_segments_are_bounded_without_broken_caps() {
335 for segments in [0, 1, 2, 128, u16::MAX] {
336 let expected = usize::from(segments.clamp(3, 128));
337 for (shape, per_segment) in [
338 (Mesh3d::cylinder(1.0, 2.0, WHITE, segments), 12),
339 (Mesh3d::cone(1.0, 2.0, WHITE, segments), 6),
340 ] {
341 assert_eq!(shape.mesh.indices.len(), expected * per_segment);
342 assert_eq!(shape.mesh.indices.len(), shape.mesh.vertices.len());
343 assert!(shape.mesh.vertices.iter().all(|v| v.normal.is_finite()));
344 }
345 }
346 }
347
348 #[test]
349 fn invalid_mesh_dimensions_are_rejected_before_recording() {
350 for invalid in [0.0, -1.0, f32::INFINITY, f32::NAN] {
351 assert!(std::panic::catch_unwind(|| Mesh3d::sphere(invalid, WHITE, 8, 6)).is_err());
352 assert!(
353 std::panic::catch_unwind(|| Mesh3d::cuboid(Vec3::splat(invalid), WHITE)).is_err()
354 );
355 assert!(
356 std::panic::catch_unwind(|| Mesh3d::plane(Vec2::splat(invalid), WHITE, 4)).is_err()
357 );
358 assert!(std::panic::catch_unwind(|| Mesh3d::cylinder(1.0, invalid, WHITE, 8)).is_err());
359 }
360 }
361
362 #[test]
363 fn tapered_mesh_faces_point_outward_and_caps_close() {
364 for shape in [
365 Mesh3d::cylinder(1.0, 2.0, WHITE, 8),
366 Mesh3d::cone(1.0, 2.0, WHITE, 8),
367 ] {
368 for face in shape.mesh.vertices.as_chunks::<3>().0 {
369 let center = (face[0].position + face[1].position + face[2].position) / 3.0;
370 let normal = face[0].normal.truncate();
371 assert!(normal.dot(center) > 0.0);
372 }
373 }
374 assert_eq!(
375 Mesh3d::cylinder(1.0, 2.0, WHITE, 8).mesh.indices.len(),
376 8 * 12
377 );
378 assert_eq!(Mesh3d::cone(1.0, 2.0, WHITE, 8).mesh.indices.len(), 8 * 6);
379 }
380
381 #[test]
382 fn ground_plane_has_nearby_vertices_correct_winding_and_bounded_batches() {
383 let plane = Mesh3d::plane(vec2(440.0, 440.0), WHITE, 22);
384 assert!(
385 plane
386 .mesh
387 .vertices
388 .iter()
389 .any(|v| v.position.length() < 0.001)
390 );
391 for triangle in plane.mesh.indices.as_chunks::<3>().0 {
392 let [a, b, c] = [triangle[0], triangle[1], triangle[2]]
393 .map(|i| plane.mesh.vertices[i as usize].position);
394 assert!((b - a).cross(c - a).y > 0.0);
395 }
396 assert!(Mesh3d::plane(Vec2::ONE, WHITE, u16::MAX).mesh.indices.len() < 5000);
397 }
398}