use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::bake::environment_map::RowScheduler;
use crate::bake::environment_map::source::{bake_payload, generate_sky_equirect};
use crate::bake::mesh::finish_mesh_payload;
use crate::components::{EnvironmentMap, Font, Material, ProceduralMesh, validate};
use crate::geometry::{
Vert, build_box, build_cylinder, build_extrude, build_plane, build_room_geometry, build_skybox,
build_sphere, build_terrain, water_grid,
};
const BOX_HALF_EXTENTS: [f32; 3] = [0.5, 0.5, 0.5];
const CYLINDER_RADIUS: f32 = 0.5;
const CYLINDER_HEIGHT: f32 = 1.0;
const CYLINDER_SEGMENTS: u32 = 16;
const SPHERE_RADIUS: f32 = 1.0;
const SPHERE_RINGS: u32 = 12;
const SPHERE_SEGMENTS: u32 = 16;
const TERRAIN_SUBDIVISIONS: u32 = 64;
const TERRAIN_AMPLITUDE: f32 = 4.0;
const SKYBOX_SIZE: f32 = 490.0;
const EXTRUDE_HEIGHT: f32 = 1.0;
const EXTRUDE_CORNER_RADIUS: f32 = 0.0;
const EXTRUDE_CORNER_SEGMENTS: u32 = 8;
const WATER_SUBDIVISIONS: u32 = 64;
pub fn procedural_mesh(mesh: &ProceduralMesh) -> Result<Vec<u8>, String> {
let (vertices, indices): (Vec<Vert>, Vec<u16>) = match mesh.generator.as_str() {
"room" => build_room_geometry(mesh.half_width, mesh.half_depth, 0.0, mesh.ceiling_height),
"box" => build_box(mesh.half_extents.unwrap_or(BOX_HALF_EXTENTS)),
"cylinder" => build_cylinder(
mesh.radius.unwrap_or(CYLINDER_RADIUS),
mesh.height.unwrap_or(CYLINDER_HEIGHT),
mesh.segments.unwrap_or(CYLINDER_SEGMENTS),
),
"plane" => build_plane(mesh.half_width, mesh.half_depth),
"sphere" => build_sphere(
mesh.radius.unwrap_or(SPHERE_RADIUS),
mesh.rings.unwrap_or(SPHERE_RINGS),
mesh.segments.unwrap_or(SPHERE_SEGMENTS),
)?,
"terrain" => build_terrain(
mesh.half_width,
mesh.half_depth,
mesh.subdivisions.unwrap_or(TERRAIN_SUBDIVISIONS),
mesh.amplitude.unwrap_or(TERRAIN_AMPLITUDE),
)?,
"skybox" => build_skybox(mesh.size.unwrap_or(SKYBOX_SIZE)),
"extrude" => {
let profile = mesh
.profile
.as_deref()
.ok_or("the `extrude` generator needs a `profile` of [x, z] points")?;
build_extrude(
profile,
mesh.height.unwrap_or(EXTRUDE_HEIGHT),
mesh.corner_radius.unwrap_or(EXTRUDE_CORNER_RADIUS),
mesh.corner_segments.unwrap_or(EXTRUDE_CORNER_SEGMENTS),
)?
}
"water_grid" => water_grid::build_water_grid(
mesh.half_width,
mesh.half_depth,
mesh.subdivisions.unwrap_or(WATER_SUBDIVISIONS),
)?,
"heightfield" => {
return Err(
"the `heightfield` generator reads a source image; compile it with the \
cook module"
.to_string(),
);
}
"" => return Err("a ProceduralMesh needs a `generator`".to_string()),
other => return Err(alloc::format!("unknown mesh generator '{other}'")),
};
finish_mesh_payload(vertices, indices, mesh.lod_levels, &mesh.lod_distances)
}
pub fn environment_map<S: RowScheduler>(map: &EnvironmentMap, rows: &S) -> Result<Vec<u8>, String> {
if !map.source.is_empty() {
return Err(
"an EnvironmentMap with a `source` reads a panorama file; compile it with the \
cook module"
.to_string(),
);
}
match map.generator.as_str() {
"sky" => {}
"" => return Err("an EnvironmentMap needs a `source` or a `generator`".to_string()),
other => return Err(alloc::format!("unknown EnvironmentMap generator '{other}'")),
}
crate::bake::environment_map::check_sizes(map)?;
Ok(bake_payload(
&generate_sky_equirect(),
map.prefilter_face_size,
map.irradiance_face_size,
map.prefilter_samples,
map.prefilter_clamp,
rows,
))
}
pub fn font(font: &Font) -> Result<Vec<u8>, String> {
if !font.path.is_empty() {
return Err(
"a Font with a `path` reads a TTF file; compile it with the cook module".to_string(),
);
}
crate::bake::font::compile(
crate::bake::font::BUILTIN_FONT_BYTES,
font.size_px,
"<built-in>",
)
}
pub fn material(material: Material) -> Result<Vec<u8>, String> {
postcard::to_allocvec(&validate::material(material))
.map_err(|e| alloc::format!("Material serialise: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bake::environment_map::Serial;
use crate::gfx::mesh_payload;
fn mesh(generator: &str) -> ProceduralMesh {
ProceduralMesh {
generator: generator.into(),
..Default::default()
}
}
#[test]
fn every_reachable_generator_bakes_a_readable_payload() {
let mut extrude = mesh("extrude");
extrude.profile = Some(alloc::vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]]);
for m in [
mesh("room"),
mesh("box"),
mesh("cylinder"),
mesh("plane"),
mesh("sphere"),
mesh("terrain"),
mesh("skybox"),
mesh("water_grid"),
extrude,
] {
let payload = procedural_mesh(&m).unwrap_or_else(|e| panic!("{}: {e}", m.generator));
let read = mesh_payload::deserialise(&payload)
.unwrap_or_else(|e| panic!("{}: {e}", m.generator));
assert!(!read.0.is_empty(), "{} has vertices", m.generator);
}
}
#[test]
fn an_unset_optional_argument_falls_back_to_the_authored_default() {
let payload = procedural_mesh(&mesh("box")).expect("a box bakes");
let (vertices, indices) = build_box(BOX_HALF_EXTENTS);
let expected = finish_mesh_payload(vertices, indices, 1, &[]).expect("the same box packs");
assert_eq!(payload, expected);
}
#[test]
fn a_generator_that_needs_an_importer_says_so() {
for (m, needle) in [
(mesh("heightfield"), "source image"),
(mesh(""), "needs a `generator`"),
(mesh("nonesuch"), "unknown mesh generator"),
(mesh("extrude"), "needs a `profile`"),
] {
let err = procedural_mesh(&m).expect_err("not bakeable");
assert!(err.contains(needle), "{err}");
}
}
#[test]
fn the_sky_generator_bakes_an_environment_payload() {
let map = EnvironmentMap {
generator: "sky".into(),
prefilter_face_size: 16,
irradiance_face_size: 8,
prefilter_samples: 4,
..Default::default()
};
let payload = environment_map(&map, &Serial).expect("the sky bakes");
let view =
crate::bake::environment_map::deserialise(&payload).expect("the payload reads back");
assert_eq!(view.irradiance_face, 8);
assert_eq!(view.prefilter_face, 16);
}
#[test]
fn an_environment_map_reports_what_it_cannot_bake() {
let with_source = EnvironmentMap {
source: "studio.hdr".into(),
..Default::default()
};
let err = environment_map(&with_source, &Serial).expect_err("a file source");
assert!(err.contains("cook module"), "{err}");
let blank = EnvironmentMap::default();
let err = environment_map(&blank, &Serial).expect_err("nothing to bake");
assert!(err.contains("needs a `source` or a `generator`"), "{err}");
let unknown = EnvironmentMap {
generator: "swamp".into(),
..Default::default()
};
let err = environment_map(&unknown, &Serial).expect_err("unknown generator");
assert!(err.contains("unknown EnvironmentMap generator"), "{err}");
let oversized = EnvironmentMap {
generator: "sky".into(),
prefilter_face_size: 3,
..Default::default()
};
let err = environment_map(&oversized, &Serial).expect_err("out of range");
assert!(err.contains("prefilter_face_size"), "{err}");
}
#[test]
fn the_builtin_face_rasterises_and_a_file_one_does_not() {
let payload = font(&Font {
size_px: 12,
..Default::default()
})
.expect("the built-in face bakes");
let (_, _, _, size_px, _, metrics) =
crate::bake::font::deserialise(&payload).expect("the atlas reads back");
assert_eq!(size_px, 12);
assert!(!metrics.is_empty());
let err = font(&Font {
path: "assets/face.ttf".into(),
..Default::default()
})
.expect_err("a file face");
assert!(err.contains("cook module"), "{err}");
}
#[test]
fn a_material_bakes_its_clamped_parameters() {
let bytes = material(Material {
roughness: 4.0,
see_through: true,
..Default::default()
})
.expect("a material bakes");
let read: Material = postcard::from_bytes(&bytes).expect("the bytes read back");
assert_eq!(read.roughness, 1.0);
assert!(read.transparent, "see-through implies transparent");
}
}