1use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16use crate::bake::environment_map::RowScheduler;
17use crate::bake::environment_map::source::{HdrImage, bake_payload, generate_sky_equirect};
18use crate::bake::environment_map::stars::generate_stars_equirect;
19use crate::bake::mesh::{finish_mesh_payload, vertices_from_data};
20use crate::components::{EnvironmentMap, Font, Material, Mesh, ProceduralMesh, validate};
21use crate::geometry::{
22 Vert, build_box, build_cylinder, build_extrude, build_plane, build_room_geometry, build_skybox,
23 build_sphere, build_terrain, water_grid,
24};
25
26const BOX_HALF_EXTENTS: [f32; 3] = [0.5, 0.5, 0.5];
29const CYLINDER_RADIUS: f32 = 0.5;
30const CYLINDER_HEIGHT: f32 = 1.0;
31const CYLINDER_SEGMENTS: u32 = 16;
32const SPHERE_RADIUS: f32 = 1.0;
33const SPHERE_RINGS: u32 = 12;
34const SPHERE_SEGMENTS: u32 = 16;
35const TERRAIN_SUBDIVISIONS: u32 = 64;
36const TERRAIN_AMPLITUDE: f32 = 4.0;
37const SKYBOX_SIZE: f32 = 490.0;
38const EXTRUDE_HEIGHT: f32 = 1.0;
39const EXTRUDE_CORNER_RADIUS: f32 = 0.0;
40const EXTRUDE_CORNER_SEGMENTS: u32 = 8;
41const WATER_SUBDIVISIONS: u32 = 64;
42
43pub fn procedural_mesh(mesh: &ProceduralMesh) -> Result<Vec<u8>, String> {
45 let (vertices, indices): (Vec<Vert>, Vec<u16>) = match mesh.generator.as_str() {
46 "room" => build_room_geometry(mesh.half_width, mesh.half_depth, 0.0, mesh.ceiling_height),
47 "box" => build_box(mesh.half_extents.unwrap_or(BOX_HALF_EXTENTS)),
48 "cylinder" => build_cylinder(
49 mesh.radius.unwrap_or(CYLINDER_RADIUS),
50 mesh.height.unwrap_or(CYLINDER_HEIGHT),
51 mesh.segments.unwrap_or(CYLINDER_SEGMENTS),
52 ),
53 "plane" => build_plane(mesh.half_width, mesh.half_depth),
54 "sphere" => build_sphere(
55 mesh.radius.unwrap_or(SPHERE_RADIUS),
56 mesh.rings.unwrap_or(SPHERE_RINGS),
57 mesh.segments.unwrap_or(SPHERE_SEGMENTS),
58 )?,
59 "terrain" => build_terrain(
60 mesh.half_width,
61 mesh.half_depth,
62 mesh.subdivisions.unwrap_or(TERRAIN_SUBDIVISIONS),
63 mesh.amplitude.unwrap_or(TERRAIN_AMPLITUDE),
64 )?,
65 "skybox" => build_skybox(mesh.size.unwrap_or(SKYBOX_SIZE)),
66 "extrude" => {
67 let profile = mesh
68 .profile
69 .as_deref()
70 .ok_or("the `extrude` generator needs a `profile` of [x, z] points")?;
71 build_extrude(
72 profile,
73 mesh.height.unwrap_or(EXTRUDE_HEIGHT),
74 mesh.corner_radius.unwrap_or(EXTRUDE_CORNER_RADIUS),
75 mesh.corner_segments.unwrap_or(EXTRUDE_CORNER_SEGMENTS),
76 )?
77 }
78 "water_grid" => water_grid::build_water_grid(
79 mesh.half_width,
80 mesh.half_depth,
81 mesh.subdivisions.unwrap_or(WATER_SUBDIVISIONS),
82 )?,
83 "heightfield" => {
86 return Err(
87 "the `heightfield` generator reads a source image; compile it with the \
88 cook module"
89 .to_string(),
90 );
91 }
92 "" => return Err("a ProceduralMesh needs a `generator`".to_string()),
93 other => return Err(alloc::format!("unknown mesh generator '{other}'")),
94 };
95 finish_mesh_payload(vertices, indices, mesh.lod_levels, &mesh.lod_distances)
96}
97
98pub fn mesh(mesh: &Mesh) -> Result<Vec<u8>, String> {
101 if !mesh.source.is_empty() {
102 return Err(
103 "a Mesh with a `source` reads a model file; compile it with the cook module"
104 .to_string(),
105 );
106 }
107 if mesh.vertices.is_empty() || mesh.indices.is_empty() {
108 return Err("a Mesh needs `vertices` and `indices`".to_string());
109 }
110 if !mesh.indices.len().is_multiple_of(3) {
111 return Err(alloc::format!(
112 "a Mesh's indices come in triangles; {} is not a multiple of 3",
113 mesh.indices.len()
114 ));
115 }
116 let vertices = vertices_from_data(&mesh.vertices, &mesh.indices)?;
117 finish_mesh_payload(
118 vertices,
119 mesh.indices.clone(),
120 mesh.lod_levels,
121 &mesh.lod_distances,
122 )
123}
124
125pub fn environment_map<S: RowScheduler>(map: &EnvironmentMap, rows: &S) -> Result<Vec<u8>, String> {
128 if !map.source.is_empty() {
129 return Err(
130 "an EnvironmentMap with a `source` reads a panorama file; compile it with the \
131 cook module"
132 .to_string(),
133 );
134 }
135 let generate: fn() -> HdrImage = match map.generator.as_str() {
136 "sky" => generate_sky_equirect,
137 "stars" => generate_stars_equirect,
138 "" => return Err("an EnvironmentMap needs a `source` or a `generator`".to_string()),
139 other => return Err(alloc::format!("unknown EnvironmentMap generator '{other}'")),
140 };
141 crate::bake::environment_map::check_sizes(map)?;
142 Ok(bake_payload(
143 &generate(),
144 map.prefilter_face_size,
145 map.irradiance_face_size,
146 map.prefilter_samples,
147 map.prefilter_clamp,
148 rows,
149 ))
150}
151
152pub fn font(font: &Font) -> Result<Vec<u8>, String> {
154 if !font.path.is_empty() {
155 return Err(
156 "a Font with a `path` reads a TTF file; compile it with the cook module".to_string(),
157 );
158 }
159 crate::bake::font::compile(
160 crate::bake::font::BUILTIN_FONT_BYTES,
161 font.size_px,
162 "<built-in>",
163 )
164}
165
166pub fn material(material: Material) -> Result<Vec<u8>, String> {
169 postcard::to_allocvec(&validate::material(material))
170 .map_err(|e| alloc::format!("Material serialise: {e}"))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::bake::environment_map::Serial;
177 use crate::gfx::mesh_payload;
178
179 fn mesh(generator: &str) -> ProceduralMesh {
180 ProceduralMesh {
181 generator: generator.into(),
182 ..Default::default()
183 }
184 }
185
186 #[test]
189 fn every_reachable_generator_bakes_a_readable_payload() {
190 let mut extrude = mesh("extrude");
191 extrude.profile = Some(alloc::vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]]);
192 for m in [
193 mesh("room"),
194 mesh("box"),
195 mesh("cylinder"),
196 mesh("plane"),
197 mesh("sphere"),
198 mesh("terrain"),
199 mesh("skybox"),
200 mesh("water_grid"),
201 extrude,
202 ] {
203 let payload = procedural_mesh(&m).unwrap_or_else(|e| panic!("{}: {e}", m.generator));
204 let read = mesh_payload::deserialise(&payload)
205 .unwrap_or_else(|e| panic!("{}: {e}", m.generator));
206 assert!(!read.0.is_empty(), "{} has vertices", m.generator);
207 }
208 }
209
210 #[test]
214 fn an_unset_optional_argument_falls_back_to_the_authored_default() {
215 let payload = procedural_mesh(&mesh("box")).expect("a box bakes");
216 let (vertices, indices) = build_box(BOX_HALF_EXTENTS);
217 let expected = finish_mesh_payload(vertices, indices, 1, &[]).expect("the same box packs");
218 assert_eq!(payload, expected);
219 }
220
221 #[test]
222 fn a_generator_that_needs_an_importer_says_so() {
223 for (m, needle) in [
224 (mesh("heightfield"), "source image"),
225 (mesh(""), "needs a `generator`"),
226 (mesh("nonesuch"), "unknown mesh generator"),
227 (mesh("extrude"), "needs a `profile`"),
228 ] {
229 let err = procedural_mesh(&m).expect_err("not bakeable");
230 assert!(err.contains(needle), "{err}");
231 }
232 }
233
234 fn triangle() -> Mesh {
235 let vd = |pos: [f32; 3], uv: [f32; 2]| crate::components::VertexData {
236 pos,
237 color: [1.0; 3],
238 uv,
239 };
240 Mesh {
241 vertices: alloc::vec![
242 vd([0.0, 0.0, 0.0], [0.0, 0.0]),
243 vd([1.0, 0.0, 0.0], [1.0, 0.0]),
244 vd([0.0, 1.0, 0.0], [0.0, 1.0]),
245 ],
246 indices: alloc::vec![0, 1, 2],
247 ..Default::default()
248 }
249 }
250
251 #[test]
252 fn raw_geometry_bakes_with_derived_normals() {
253 let payload = super::mesh(&triangle()).expect("a triangle bakes");
254 let (verts, indices) = mesh_payload::deserialise(&payload).expect("the payload reads back");
255 assert_eq!(indices, alloc::vec![0, 1, 2]);
256 assert!(verts.iter().all(|v| (v.normal[2] - 1.0).abs() < 1e-5));
257 }
258
259 #[test]
260 fn raw_geometry_reports_what_it_cannot_bake() {
261 let sourced = Mesh {
262 source: "chair.glb".into(),
263 ..Default::default()
264 };
265 let err = super::mesh(&sourced).expect_err("a file source");
266 assert!(err.contains("cook module"), "{err}");
267
268 let err = super::mesh(&Mesh::default()).expect_err("nothing to bake");
269 assert!(err.contains("needs `vertices` and `indices`"), "{err}");
270
271 let mut ragged = triangle();
272 ragged.indices.push(0);
273 let err = super::mesh(&ragged).expect_err("a partial triangle");
274 assert!(err.contains("multiple of 3"), "{err}");
275
276 let mut past = triangle();
277 past.indices[2] = 7;
278 let err = super::mesh(&past).expect_err("an index past the list");
279 assert!(err.contains("indexes past"), "{err}");
280 }
281
282 #[test]
283 fn the_sky_generator_bakes_an_environment_payload() {
284 let map = EnvironmentMap {
285 generator: "sky".into(),
286 prefilter_face_size: 16,
287 irradiance_face_size: 8,
288 prefilter_samples: 4,
289 ..Default::default()
290 };
291 let payload = environment_map(&map, &Serial).expect("the sky bakes");
292 let view =
293 crate::bake::environment_map::deserialise(&payload).expect("the payload reads back");
294 assert_eq!(view.irradiance_face, 8);
295 assert_eq!(view.prefilter_face, 16);
296 }
297
298 #[test]
299 fn an_environment_map_reports_what_it_cannot_bake() {
300 let with_source = EnvironmentMap {
301 source: "studio.hdr".into(),
302 ..Default::default()
303 };
304 let err = environment_map(&with_source, &Serial).expect_err("a file source");
305 assert!(err.contains("cook module"), "{err}");
306
307 let blank = EnvironmentMap::default();
308 let err = environment_map(&blank, &Serial).expect_err("nothing to bake");
309 assert!(err.contains("needs a `source` or a `generator`"), "{err}");
310
311 let unknown = EnvironmentMap {
312 generator: "swamp".into(),
313 ..Default::default()
314 };
315 let err = environment_map(&unknown, &Serial).expect_err("unknown generator");
316 assert!(err.contains("unknown EnvironmentMap generator"), "{err}");
317
318 let oversized = EnvironmentMap {
319 generator: "sky".into(),
320 prefilter_face_size: 3,
321 ..Default::default()
322 };
323 let err = environment_map(&oversized, &Serial).expect_err("out of range");
324 assert!(err.contains("prefilter_face_size"), "{err}");
325 }
326
327 #[test]
328 fn the_builtin_face_rasterises_and_a_file_one_does_not() {
329 let payload = font(&Font {
330 size_px: 12,
331 ..Default::default()
332 })
333 .expect("the built-in face bakes");
334 let (_, _, _, size_px, _, metrics) =
335 crate::bake::font::deserialise(&payload).expect("the atlas reads back");
336 assert_eq!(size_px, 12);
337 assert!(!metrics.is_empty());
338
339 let err = font(&Font {
340 path: "assets/face.ttf".into(),
341 ..Default::default()
342 })
343 .expect_err("a file face");
344 assert!(err.contains("cook module"), "{err}");
345 }
346
347 #[test]
350 fn a_material_bakes_its_clamped_parameters() {
351 let bytes = material(Material {
352 roughness: 4.0,
353 see_through: true,
354 ..Default::default()
355 })
356 .expect("a material bakes");
357 let read: Material = postcard::from_bytes(&bytes).expect("the bytes read back");
358 assert_eq!(read.roughness, 1.0);
359 assert!(read.transparent, "see-through implies transparent");
360 }
361}