nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
//! Material registry resource.

use crate::ecs::asset_id::MaterialId;
use crate::ecs::generational_registry::GenerationalRegistry;
use crate::ecs::material::components::Material;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};

/// Global registry of named materials.
///
/// Materials are stored by name and assigned generational IDs for safe references.
/// Includes default color materials (Red, Green, Blue, etc.) that cannot be removed.
#[derive(Clone)]
pub struct MaterialRegistry {
    /// The underlying generational storage.
    pub registry: GenerationalRegistry<Material>,
    /// Indices of built-in materials that should not be garbage collected.
    pub protected_indices: Vec<u32>,
    /// Maps content hash to material name for deduplication.
    pub content_hash_to_name: HashMap<u64, String>,
}

impl Default for MaterialRegistry {
    fn default() -> Self {
        let mut instance = Self {
            registry: GenerationalRegistry::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) = instance.registry.insert(name.to_string(), material);
            instance.protected_indices.push(index);
        }

        instance
    }
}

/// Inserts a material into the registry, returning its ID.
pub fn material_registry_insert(
    registry: &mut MaterialRegistry,
    name: String,
    material: Material,
) -> MaterialId {
    let (index, generation) = registry.registry.insert(name, material);
    MaterialId { index, generation }
}

/// Looks up a material ID by name.
pub fn material_registry_lookup_id(registry: &MaterialRegistry, name: &str) -> Option<MaterialId> {
    registry
        .registry
        .lookup_index(name)
        .map(|(index, generation)| MaterialId { index, generation })
}

/// Removes materials with zero references (except protected materials).
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()
        {
            if let Some(name) = registry.registry.index_to_name[index].take() {
                registry.registry.name_to_index.remove(&name);
                removed.push(name);
            }
            registry.registry.entries[index] = None;
            registry.registry.free_indices.push(index as u32);
        }
    }

    removed
}

/// Iterates over all materials as (name, material) pairs.
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 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.base_texture.hash(&mut hasher);
    material.base_texture_uv_set.hash(&mut hasher);
    material.emissive_texture.hash(&mut hasher);
    material.emissive_texture_uv_set.hash(&mut hasher);
    material.normal_texture.hash(&mut hasher);
    material.normal_texture_uv_set.hash(&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);
    material.metallic_roughness_texture_uv_set.hash(&mut hasher);
    material.occlusion_texture.hash(&mut hasher);
    material.occlusion_texture_uv_set.hash(&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);
    for component in &material.uv_scale {
        component.to_bits().hash(&mut hasher);
    }
    material.transmission_factor.to_bits().hash(&mut hasher);
    material.transmission_texture.hash(&mut hasher);
    material.transmission_texture_uv_set.hash(&mut hasher);
    material.thickness.to_bits().hash(&mut hasher);
    material.thickness_texture.hash(&mut hasher);
    material.thickness_texture_uv_set.hash(&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);
    material.specular_texture_uv_set.hash(&mut hasher);
    material.specular_color_texture.hash(&mut hasher);
    material.specular_color_texture_uv_set.hash(&mut hasher);
    material.emissive_strength.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) {
        return existing_name.clone();
    }
    registry
        .content_hash_to_name
        .insert(content_hash, fallback_name.clone());
    material_registry_insert(registry, fallback_name.clone(), material);
    fallback_name
}