1use glam::Vec3;
8use unlit3dx::prelude::*;
9
10use crate::render::{OwnedLitMeshDesc, WHITE_COLOR, cuboid_mesh, uv_sphere};
11
12pub const BALL_RADIUS: f32 = 0.8;
14
15pub const BALL_COLORS: [[u8; 4]; 4] = [
17 [228, 92, 74, 255],
18 [96, 178, 92, 255],
19 [86, 132, 228, 255],
20 [232, 192, 76, 255],
21];
22
23pub const PATTERN_SIZE: u32 = 128;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum ShapeKind {
32 Ball,
34 Cube,
36 Cylinder,
38 Cone,
40}
41
42pub const SHAPE_KINDS: [ShapeKind; 4] = [
44 ShapeKind::Ball,
45 ShapeKind::Cube,
46 ShapeKind::Cylinder,
47 ShapeKind::Cone,
48];
49
50pub fn shape_kind(index: usize) -> ShapeKind {
52 SHAPE_KINDS[index % SHAPE_KINDS.len()]
53}
54
55fn tinted(color: [u8; 4], factor: f32) -> [u8; 4] {
57 [
58 (f32::from(color[0]) * factor).round().clamp(0.0, 255.0) as u8,
59 (f32::from(color[1]) * factor).round().clamp(0.0, 255.0) as u8,
60 (f32::from(color[2]) * factor).round().clamp(0.0, 255.0) as u8,
61 color[3],
62 ]
63}
64
65#[must_use]
70pub fn pattern_texture(kind: ShapeKind, color: [u8; 4]) -> Vec<[u8; 4]> {
71 let accent = tinted(color, 0.72);
72 let size = PATTERN_SIZE;
73 let block = match kind {
74 ShapeKind::Cube => size / 4,
76 ShapeKind::Cylinder | ShapeKind::Cone => size / 8,
78 ShapeKind::Ball => size / 6,
80 };
81 let mut pixels = Vec::with_capacity((size * size) as usize);
82 for y in 0..size {
83 for x in 0..size {
84 let on = match kind {
85 ShapeKind::Cube => (x / block + y / block).is_multiple_of(2),
87 ShapeKind::Cylinder => (x / block).is_multiple_of(2),
90 ShapeKind::Cone => (y / block).is_multiple_of(2),
93 ShapeKind::Ball => {
94 let cx = x % block;
97 let cy = y % block;
98 let half = block / 2;
99 let dx = cx.abs_diff(half);
100 let dy = cy.abs_diff(half);
101 dx * dx + dy * dy < half * half / 2
102 }
103 };
104 pixels.push(if on { accent } else { color });
105 }
106 }
107 pixels
108}
109
110#[must_use]
113pub fn shape_mesh(kind: ShapeKind) -> OwnedLitMeshDesc {
114 match kind {
115 ShapeKind::Ball => uv_sphere(BALL_RADIUS, 32, 22),
116 ShapeKind::Cube => cuboid_mesh(Vec3::splat(BALL_RADIUS * 0.8)),
117 ShapeKind::Cylinder => into_desc(uv_cylinder(BALL_RADIUS * 0.72, BALL_RADIUS, 24)),
118 ShapeKind::Cone => into_desc(uv_cone(BALL_RADIUS * 0.9, BALL_RADIUS, 24)),
119 }
120}
121
122fn into_desc(mesh: RawMesh) -> OwnedLitMeshDesc {
125 let (positions, uvs, colors, indices) = mesh;
126 OwnedLitMeshDesc {
127 positions,
128 uvs: Some(uvs),
129 colors: Some(colors),
130 indices: Some(indices),
131 }
132}
133
134#[must_use]
137pub fn shape_body(kind: ShapeKind) -> BodyShape {
138 match kind {
139 ShapeKind::Ball => BodyShape::Ball(BALL_RADIUS),
140 ShapeKind::Cube => BodyShape::Cuboid(Vec3::splat(BALL_RADIUS * 0.8)),
141 ShapeKind::Cylinder => BodyShape::Cylinder {
142 half_height: BALL_RADIUS,
143 radius: BALL_RADIUS * 0.72,
144 },
145 ShapeKind::Cone => BodyShape::Cone {
146 half_height: BALL_RADIUS,
147 radius: BALL_RADIUS * 0.9,
148 },
149 }
150}
151
152pub type RawMesh = (Vec<[f32; 3]>, Vec<[f32; 2]>, Vec<[u8; 4]>, Vec<u32>);
154
155fn lathe(rings: &[(f32, f32); 2], caps: (bool, bool), slices: usize) -> RawMesh {
159 let rows = rings.len();
160 let ring_len = slices + 1;
161 let vertices = ring_len * rows + ring_len * (usize::from(caps.0) + usize::from(caps.1));
162 let mut positions = Vec::with_capacity(vertices);
163 let mut uvs = Vec::with_capacity(vertices);
164 let mut colors = Vec::with_capacity(vertices);
165 let mut indices = Vec::with_capacity(
166 (rows - 1) * slices * 6 + slices * 3 * (usize::from(caps.0) + usize::from(caps.1)),
167 );
168 for (row, &(radius, y)) in rings.iter().enumerate() {
172 let v = row as f32 / (rows - 1).max(1) as f32;
173 for slice in 0..=slices {
174 let u = slice as f32 / slices as f32;
175 let (sin, cos) = (u * std::f32::consts::TAU).sin_cos();
176 positions.push([radius * cos, y, radius * sin]);
177 uvs.push([u, v]);
178 colors.push(WHITE_COLOR);
179 }
180 }
181 for row in 0..rows - 1 {
182 for slice in 0..slices {
183 let top = (row * ring_len + slice) as u32;
184 let bottom = top + ring_len as u32;
185 indices.extend_from_slice(&[top, top + 1, bottom, top + 1, bottom + 1, bottom]);
186 }
187 }
188 let cap = |lathe: &mut RawMesh, y: f32, up: bool| {
192 let base = lathe.0.len() as u32;
193 let centre = base;
194 lathe.0.push([0.0, y, 0.0]);
195 lathe.1.push([0.5, 0.5]);
196 lathe.2.push(WHITE_COLOR);
197 let &(radius, ring_y) = if up {
198 rings.first().expect("a top ring")
199 } else {
200 rings.last().expect("a bottom ring")
201 };
202 for slice in 0..=slices {
203 let u = slice as f32 / slices as f32;
204 let (sin, cos) = (u * std::f32::consts::TAU).sin_cos();
205 lathe.0.push([radius * cos, ring_y, radius * sin]);
206 lathe.1.push([0.5 + 0.5 * cos, 0.5 + 0.5 * sin]);
207 lathe.2.push(WHITE_COLOR);
208 }
209 for slice in 0..slices {
210 let one = centre + 1 + slice as u32;
211 let two = one + 1;
212 let fan = if up {
213 [centre, two, one]
214 } else {
215 [centre, one, two]
216 };
217 lathe.3.extend_from_slice(&fan);
218 }
219 };
220 let mut lathe = (positions, uvs, colors, indices);
221 if caps.0 {
222 cap(&mut lathe, rings[0].1, true);
223 }
224 if caps.1 {
225 cap(&mut lathe, rings[rows - 1].1, false);
226 }
227 lathe
228}
229
230pub fn uv_cylinder(radius: f32, half_height: f32, slices: usize) -> RawMesh {
233 lathe(
234 &[(radius, half_height), (radius, -half_height)],
235 (true, true),
236 slices,
237 )
238}
239
240pub fn uv_cone(radius: f32, half_height: f32, slices: usize) -> RawMesh {
243 lathe(
244 &[(0.0, half_height), (radius, -half_height)],
245 (false, true),
246 slices,
247 )
248}
249
250#[cfg(test)]
251pub(crate) mod tests {
252 use super::*;
253
254 pub(crate) fn assert_faces_outward(name: &str, mesh: &OwnedLitMeshDesc) {
258 let indices = mesh.indices.as_deref().unwrap_or(&[]);
259 for triangle in indices.as_chunks::<3>().0 {
260 let [a, b, c] = triangle.map(|index| Vec3::from(mesh.positions[index as usize]));
261 let normal = (b - a).cross(c - a);
262 if normal.length_squared() <= f32::EPSILON {
265 continue;
266 }
267 let center = (a + b + c) / 3.0;
268 assert!(
269 normal.dot(center) > 0.0,
270 "{name}: triangle {triangle:?} faces inward",
271 );
272 }
273 }
274
275 fn kind_name(kind: ShapeKind) -> &'static str {
277 match kind {
278 ShapeKind::Ball => "sphere",
279 ShapeKind::Cube => "cube",
280 ShapeKind::Cylinder => "cylinder",
281 ShapeKind::Cone => "cone",
282 }
283 }
284
285 #[test]
286 fn meshes_face_outward() {
287 for kind in SHAPE_KINDS {
288 assert_faces_outward(kind_name(kind), &shape_mesh(kind));
289 }
290 }
291
292 #[test]
293 fn patterns_cover_their_canvas() {
294 for kind in SHAPE_KINDS {
295 for color in BALL_COLORS {
296 let pixels = pattern_texture(kind, color);
297 assert_eq!(pixels.len(), (PATTERN_SIZE * PATTERN_SIZE) as usize);
298 let accents = pixels.iter().filter(|pixel| **pixel != color).count();
299 assert!(accents > 0, "{kind:?}: the pattern painted nothing");
300 assert!(
301 accents < pixels.len(),
302 "{kind:?}: the pattern left no base color",
303 );
304 }
305 }
306 }
307}