use crate::ecs::asset_id::{MaterialId, TextureId};
use crate::ecs::generational_registry::*;
use crate::ecs::material::components::{Material, MaterialTextureIds};
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
#[derive(Clone)]
pub struct MaterialRegistry {
pub registry: GenerationalRegistry<Material>,
pub texture_ids: Vec<MaterialTextureIds>,
pub texture_name_to_materials: HashMap<String, HashSet<u32>>,
pub pending_resolve: Vec<u32>,
pub protected_indices: Vec<u32>,
pub content_hash_to_name: HashMap<u64, String>,
}
impl Default for MaterialRegistry {
fn default() -> Self {
let mut instance = Self {
registry: GenerationalRegistry::default(),
texture_ids: Vec::new(),
texture_name_to_materials: HashMap::new(),
pending_resolve: Vec::new(),
protected_indices: Vec::new(),
content_hash_to_name: HashMap::new(),
};
let default_materials = [
(
"Default",
Material {
base_color: [0.7, 0.7, 0.7, 1.0],
..Default::default()
},
),
(
"Red",
Material {
base_color: [1.0, 0.3, 0.3, 1.0],
..Default::default()
},
),
(
"Green",
Material {
base_color: [0.3, 1.0, 0.3, 1.0],
..Default::default()
},
),
(
"Blue",
Material {
base_color: [0.3, 0.3, 1.0, 1.0],
..Default::default()
},
),
(
"Yellow",
Material {
base_color: [1.0, 1.0, 0.3, 1.0],
..Default::default()
},
),
(
"Magenta",
Material {
base_color: [1.0, 0.3, 1.0, 1.0],
..Default::default()
},
),
(
"Cyan",
Material {
base_color: [0.3, 1.0, 1.0, 1.0],
..Default::default()
},
),
(
"White",
Material {
base_color: [1.0, 1.0, 1.0, 1.0],
..Default::default()
},
),
(
"Black",
Material {
base_color: [0.1, 0.1, 0.1, 1.0],
..Default::default()
},
),
];
for (name, material) in default_materials {
let (index, _generation) =
registry_insert(&mut instance.registry, name.to_string(), material);
instance.protected_indices.push(index);
}
instance
}
}
pub fn material_registry_insert(
registry: &mut MaterialRegistry,
name: String,
material: Material,
) -> MaterialId {
let new_texture_names = collect_material_texture_names(&material);
if let Some(&existing_index) = registry.registry.name_to_index.get(&name)
&& let Some(existing_material) = registry.registry.entries[existing_index as usize].as_ref()
{
let previous_texture_names = collect_material_texture_names(existing_material);
for previous_name in previous_texture_names {
if let Some(materials) = registry.texture_name_to_materials.get_mut(&previous_name) {
materials.remove(&existing_index);
if materials.is_empty() {
registry.texture_name_to_materials.remove(&previous_name);
}
}
}
}
let (index, generation) = registry_insert(&mut registry.registry, name, material);
let slot = index as usize;
if slot >= registry.texture_ids.len() {
registry
.texture_ids
.resize_with(slot + 1, MaterialTextureIds::default);
}
for texture_name in new_texture_names {
registry
.texture_name_to_materials
.entry(texture_name)
.or_default()
.insert(index);
}
registry.pending_resolve.push(index);
MaterialId::new(index, generation)
}
fn collect_material_texture_names(material: &Material) -> Vec<String> {
[
&material.base_texture,
&material.emissive_texture,
&material.normal_texture,
&material.metallic_roughness_texture,
&material.occlusion_texture,
&material.transmission_texture,
&material.thickness_texture,
&material.specular_texture,
&material.specular_color_texture,
&material.diffuse_transmission_texture,
&material.diffuse_transmission_color_texture,
&material.anisotropy_texture,
&material.iridescence_texture,
&material.iridescence_thickness_texture,
&material.sheen_color_texture,
&material.sheen_roughness_texture,
&material.clearcoat_texture,
&material.clearcoat_roughness_texture,
&material.clearcoat_normal_texture,
]
.into_iter()
.filter_map(|opt| opt.clone())
.collect()
}
fn resolve_one_material(
registry: &mut MaterialRegistry,
slot: usize,
texture_cache: &crate::render::wgpu::texture_cache::TextureCache,
) {
let lookup = |opt: &Option<String>| -> Option<TextureId> {
opt.as_deref()
.and_then(|name| registry_lookup_index(&texture_cache.registry, name))
.map(|(index, generation)| TextureId::new(index, generation))
};
let Some(material) = registry
.registry
.entries
.get(slot)
.and_then(|entry| entry.as_ref())
else {
if let Some(stale) = registry.texture_ids.get_mut(slot) {
*stale = MaterialTextureIds::default();
}
return;
};
let resolved = MaterialTextureIds {
base: lookup(&material.base_texture),
emissive: lookup(&material.emissive_texture),
normal: lookup(&material.normal_texture),
metallic_roughness: lookup(&material.metallic_roughness_texture),
occlusion: lookup(&material.occlusion_texture),
transmission: lookup(&material.transmission_texture),
thickness: lookup(&material.thickness_texture),
specular: lookup(&material.specular_texture),
specular_color: lookup(&material.specular_color_texture),
diffuse_transmission: lookup(&material.diffuse_transmission_texture),
diffuse_transmission_color: lookup(&material.diffuse_transmission_color_texture),
anisotropy: lookup(&material.anisotropy_texture),
iridescence: lookup(&material.iridescence_texture),
iridescence_thickness: lookup(&material.iridescence_thickness_texture),
sheen_color: lookup(&material.sheen_color_texture),
sheen_roughness: lookup(&material.sheen_roughness_texture),
clearcoat: lookup(&material.clearcoat_texture),
clearcoat_roughness: lookup(&material.clearcoat_roughness_texture),
clearcoat_normal: lookup(&material.clearcoat_normal_texture),
};
if slot >= registry.texture_ids.len() {
registry
.texture_ids
.resize_with(slot + 1, MaterialTextureIds::default);
}
registry.texture_ids[slot] = resolved;
}
pub fn material_registry_resolve_uploaded_textures(
registry: &mut MaterialRegistry,
texture_cache: &crate::render::wgpu::texture_cache::TextureCache,
uploaded: &[TextureId],
) {
let mut dirty: HashSet<u32> = HashSet::new();
for id in uploaded {
let Some(name) = registry_name_for(&texture_cache.registry, id.index) else {
continue;
};
if let Some(materials) = registry.texture_name_to_materials.get(name) {
dirty.extend(materials.iter().copied());
}
}
dirty.extend(registry.pending_resolve.drain(..));
for index in dirty {
resolve_one_material(registry, index as usize, texture_cache);
}
}
pub fn material_registry_lookup_id(registry: &MaterialRegistry, name: &str) -> Option<MaterialId> {
registry_lookup_index(®istry.registry, name)
.map(|(index, generation)| MaterialId::new(index, generation))
}
pub fn material_registry_remove_unused(registry: &mut MaterialRegistry) -> Vec<String> {
let mut removed = Vec::new();
for index in 0..registry.registry.entries.len() {
if registry.protected_indices.contains(&(index as u32)) {
continue;
}
if registry.registry.reference_counts[index] == 0
&& registry.registry.entries[index].is_some()
{
let texture_names = registry.registry.entries[index]
.as_ref()
.map(collect_material_texture_names)
.unwrap_or_default();
let content_hash = registry.registry.entries[index]
.as_ref()
.map(compute_material_content_hash);
for texture_name in texture_names {
if let Some(materials) = registry.texture_name_to_materials.get_mut(&texture_name) {
materials.remove(&(index as u32));
if materials.is_empty() {
registry.texture_name_to_materials.remove(&texture_name);
}
}
}
registry
.pending_resolve
.retain(|&pending_index| pending_index != index as u32);
if let Some(name) = registry.registry.index_to_name[index].take() {
registry.registry.name_to_index.remove(&name);
if let Some(hash) = content_hash
&& let Some(mapped) = registry.content_hash_to_name.get(&hash)
&& *mapped == name
{
registry.content_hash_to_name.remove(&hash);
}
removed.push(name);
}
registry.registry.entries[index] = None;
registry.registry.free_indices.push(index as u32);
if let Some(slot) = registry.texture_ids.get_mut(index) {
*slot = MaterialTextureIds::default();
}
}
}
removed
}
pub fn material_registry_iter(
registry: &MaterialRegistry,
) -> impl Iterator<Item = (&String, &Material)> {
registry
.registry
.index_to_name
.iter()
.enumerate()
.filter_map(|(index, name_opt)| {
name_opt.as_ref().and_then(|name| {
registry.registry.entries[index]
.as_ref()
.map(|material| (name, material))
})
})
}
fn hash_texture_transform(
transform: &crate::ecs::material::components::TextureTransform,
hasher: &mut std::collections::hash_map::DefaultHasher,
) {
for component in &transform.offset {
component.to_bits().hash(hasher);
}
transform.rotation.to_bits().hash(hasher);
for component in &transform.scale {
component.to_bits().hash(hasher);
}
transform.uv_set.hash(hasher);
}
fn compute_material_content_hash(material: &Material) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for component in &material.base_color {
component.to_bits().hash(&mut hasher);
}
for component in &material.emissive_factor {
component.to_bits().hash(&mut hasher);
}
material.alpha_mode.hash(&mut hasher);
material.alpha_cutoff.to_bits().hash(&mut hasher);
material
.blend_opaque_alpha_threshold
.to_bits()
.hash(&mut hasher);
material.base_texture.hash(&mut hasher);
hash_texture_transform(&material.base_texture_transform, &mut hasher);
material.emissive_texture.hash(&mut hasher);
hash_texture_transform(&material.emissive_texture_transform, &mut hasher);
material.normal_texture.hash(&mut hasher);
hash_texture_transform(&material.normal_texture_transform, &mut hasher);
material.normal_scale.to_bits().hash(&mut hasher);
material.normal_map_flip_y.hash(&mut hasher);
material.normal_map_two_component.hash(&mut hasher);
material.metallic_roughness_texture.hash(&mut hasher);
hash_texture_transform(&material.metallic_roughness_texture_transform, &mut hasher);
material.occlusion_texture.hash(&mut hasher);
hash_texture_transform(&material.occlusion_texture_transform, &mut hasher);
material.occlusion_strength.to_bits().hash(&mut hasher);
material.roughness.to_bits().hash(&mut hasher);
material.metallic.to_bits().hash(&mut hasher);
material.unlit.hash(&mut hasher);
material.double_sided.hash(&mut hasher);
material.transmission_factor.to_bits().hash(&mut hasher);
material.transmission_texture.hash(&mut hasher);
hash_texture_transform(&material.transmission_texture_transform, &mut hasher);
material.thickness.to_bits().hash(&mut hasher);
material.thickness_texture.hash(&mut hasher);
hash_texture_transform(&material.thickness_texture_transform, &mut hasher);
for component in &material.attenuation_color {
component.to_bits().hash(&mut hasher);
}
material.attenuation_distance.to_bits().hash(&mut hasher);
material.ior.to_bits().hash(&mut hasher);
material.specular_factor.to_bits().hash(&mut hasher);
for component in &material.specular_color_factor {
component.to_bits().hash(&mut hasher);
}
material.specular_texture.hash(&mut hasher);
hash_texture_transform(&material.specular_texture_transform, &mut hasher);
material.specular_color_texture.hash(&mut hasher);
hash_texture_transform(&material.specular_color_texture_transform, &mut hasher);
material.emissive_strength.to_bits().hash(&mut hasher);
material
.diffuse_transmission_factor
.to_bits()
.hash(&mut hasher);
material.diffuse_transmission_texture.hash(&mut hasher);
hash_texture_transform(
&material.diffuse_transmission_texture_transform,
&mut hasher,
);
for component in &material.diffuse_transmission_color_factor {
component.to_bits().hash(&mut hasher);
}
material
.diffuse_transmission_color_texture
.hash(&mut hasher);
hash_texture_transform(
&material.diffuse_transmission_color_texture_transform,
&mut hasher,
);
material.dispersion.to_bits().hash(&mut hasher);
material.anisotropy_strength.to_bits().hash(&mut hasher);
material.anisotropy_rotation.to_bits().hash(&mut hasher);
material.anisotropy_texture.hash(&mut hasher);
hash_texture_transform(&material.anisotropy_texture_transform, &mut hasher);
material.iridescence_factor.to_bits().hash(&mut hasher);
material.iridescence_texture.hash(&mut hasher);
hash_texture_transform(&material.iridescence_texture_transform, &mut hasher);
material.iridescence_ior.to_bits().hash(&mut hasher);
material
.iridescence_thickness_minimum
.to_bits()
.hash(&mut hasher);
material
.iridescence_thickness_maximum
.to_bits()
.hash(&mut hasher);
material.iridescence_thickness_texture.hash(&mut hasher);
hash_texture_transform(
&material.iridescence_thickness_texture_transform,
&mut hasher,
);
for component in &material.sheen_color_factor {
component.to_bits().hash(&mut hasher);
}
material.sheen_color_texture.hash(&mut hasher);
hash_texture_transform(&material.sheen_color_texture_transform, &mut hasher);
material.sheen_roughness_factor.to_bits().hash(&mut hasher);
material.sheen_roughness_texture.hash(&mut hasher);
hash_texture_transform(&material.sheen_roughness_texture_transform, &mut hasher);
material.clearcoat_factor.to_bits().hash(&mut hasher);
material.clearcoat_texture.hash(&mut hasher);
hash_texture_transform(&material.clearcoat_texture_transform, &mut hasher);
material
.clearcoat_roughness_factor
.to_bits()
.hash(&mut hasher);
material.clearcoat_roughness_texture.hash(&mut hasher);
hash_texture_transform(&material.clearcoat_roughness_texture_transform, &mut hasher);
material.clearcoat_normal_texture.hash(&mut hasher);
hash_texture_transform(&material.clearcoat_normal_texture_transform, &mut hasher);
material.clearcoat_normal_scale.to_bits().hash(&mut hasher);
hasher.finish()
}
pub fn material_registry_find_or_insert(
registry: &mut MaterialRegistry,
fallback_name: String,
material: Material,
) -> String {
let content_hash = compute_material_content_hash(&material);
if let Some(existing_name) = registry.content_hash_to_name.get(&content_hash).cloned() {
let still_valid = registry
.registry
.name_to_index
.get(&existing_name)
.and_then(|&index| registry.registry.entries.get(index as usize))
.is_some_and(|entry| entry.is_some());
if still_valid {
return existing_name;
}
registry.content_hash_to_name.remove(&content_hash);
}
registry
.content_hash_to_name
.insert(content_hash, fallback_name.clone());
material_registry_insert(registry, fallback_name.clone(), material);
fallback_name
}