use concinnity_core::components::Material;
use concinnity_core::ecs::{MaterialHandle, TextureHandle};
use concinnity_core::gfx::render_types::{MaterialUniforms, NO_ALBEDO_SLOT, NO_NORMAL_MAP_SLOT};
#[derive(Clone, Copy)]
pub(crate) struct MaterialEntry {
pub(crate) albedo_slot: usize,
pub(crate) normal_map_slot: usize,
pub(crate) uniforms: MaterialUniforms,
pub(crate) shader_bucket: u32,
}
impl MaterialEntry {
pub(crate) const UNTEXTURED: MaterialEntry = MaterialEntry {
albedo_slot: NO_ALBEDO_SLOT,
normal_map_slot: NO_NORMAL_MAP_SLOT,
uniforms: MaterialUniforms::DEFAULT,
shader_bucket: 0,
};
}
pub(crate) fn of(mat: &Material, texture_count: usize) -> Result<MaterialEntry, &'static str> {
let slot_of = |field: &'static str, handle: Option<TextureHandle>, unset: usize| {
let Some(handle) = handle else {
return Ok(unset);
};
let slot = handle.index();
if slot >= texture_count {
return Err(field);
}
Ok(slot)
};
let albedo_slot = slot_of("albedo", mat.albedo, NO_ALBEDO_SLOT)?;
let normal_map_slot = slot_of("normal_map", mat.normal_map, NO_NORMAL_MAP_SLOT)?;
let emissive_map_slot = slot_of("emissive_map", mat.emissive_map, 0)?;
let orm_map_slot = slot_of("orm_map", mat.orm_map, 0)?;
Ok(MaterialEntry {
albedo_slot,
normal_map_slot,
uniforms: MaterialUniforms {
roughness: mat.roughness,
metallic: mat.metallic,
alpha_cutoff: mat.alpha_cutoff,
opacity: mat.opacity,
tint: mat.tint,
_pad0: 0.0,
emissive: mat.emissive_factor,
_pad1: 0.0,
emissive_map_index: emissive_map_slot as u32,
orm_map_index: orm_map_slot as u32,
transparent: u32::from(mat.transparent),
see_through: u32::from(mat.see_through),
},
shader_bucket: mat.shader.map_or(0, |h| h.0),
})
}
pub(crate) fn resolve_material_slots(
material: Option<MaterialHandle>,
material_map: &std::collections::HashMap<MaterialHandle, MaterialEntry>,
) -> Result<MaterialEntry, MaterialHandle> {
match material {
Some(mat_id) => material_map.get(&mat_id).copied().ok_or(mat_id),
None => Ok(MaterialEntry::UNTEXTURED),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn material() -> Material {
Material {
roughness: 0.25,
metallic: 0.75,
..Default::default()
}
}
#[test]
fn unset_references_take_their_fallbacks() {
let entry = of(&material(), 4).expect("bakes");
assert_eq!(entry.albedo_slot, NO_ALBEDO_SLOT);
assert_eq!(entry.normal_map_slot, NO_NORMAL_MAP_SLOT);
assert_eq!(entry.uniforms.emissive_map_index, 0);
assert_eq!(entry.uniforms.orm_map_index, 0);
assert_eq!(entry.uniforms.roughness, 0.25);
assert_eq!(entry.uniforms.metallic, 0.75);
}
#[test]
fn a_set_reference_resolves_to_its_pool_slot() {
let mut mat = material();
mat.albedo = Some(TextureHandle(2));
mat.normal_map = Some(TextureHandle(3));
let entry = of(&mat, 4).expect("bakes");
assert_eq!(entry.albedo_slot, 2);
assert_eq!(entry.normal_map_slot, 3);
}
#[test]
fn a_reference_past_the_pool_names_its_field() {
let mut mat = material();
mat.orm_map = Some(TextureHandle(9));
assert_eq!(of(&mat, 4).err(), Some("orm_map"));
}
#[test]
fn the_shader_reference_becomes_the_draw_bucket() {
let mut mat = material();
assert_eq!(of(&mat, 0).expect("bakes").shader_bucket, 0);
mat.shader = Some(concinnity_core::ecs::ShaderHandle(3));
assert_eq!(of(&mat, 0).expect("bakes").shader_bucket, 3);
}
#[test]
fn a_draw_without_a_material_is_untextured() {
let entry = MaterialEntry::UNTEXTURED;
assert_eq!(entry.albedo_slot, NO_ALBEDO_SLOT);
assert_eq!(entry.normal_map_slot, NO_NORMAL_MAP_SLOT);
assert_eq!(entry.shader_bucket, 0);
let map = std::collections::HashMap::new();
assert_eq!(
resolve_material_slots(None, &map)
.expect("resolves")
.albedo_slot,
NO_ALBEDO_SLOT
);
}
#[test]
fn a_material_handle_must_resolve() {
let entry = of(&material(), 4).expect("bakes");
let map = std::collections::HashMap::from([(MaterialHandle(2), entry)]);
let got = resolve_material_slots(Some(MaterialHandle(2)), &map).expect("resolves");
assert_eq!(got.uniforms.roughness, 0.25);
assert_eq!(got.albedo_slot, NO_ALBEDO_SLOT, "the material's own albedo");
assert_eq!(
resolve_material_slots(Some(MaterialHandle(5)), &map).err(),
Some(MaterialHandle(5))
);
}
}