nightshade 0.13.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::world::{RENDER_MESH, SKIN, World};
use freecs::Entity;
use nalgebra_glm::Mat4;
use std::collections::HashMap;

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuSkinData {
    pub joint_count: u32,
    pub base_bone_index: u32,
    pub base_ibm_index: u32,
    pub base_output_index: u32,
}

pub struct SkinningUploadData {
    pub bone_transforms: Vec<Mat4>,
    pub inverse_bind_matrices: Vec<Mat4>,
    pub skin_data: Vec<GpuSkinData>,
    pub entity_skin_indices: HashMap<Entity, u32>,
    pub total_joints: u32,
}

impl SkinningUploadData {
    pub fn get_joint_offset(&self, skin_index: u32) -> u32 {
        if let Some(skin_data) = self.skin_data.get(skin_index as usize) {
            skin_data.base_output_index
        } else {
            0
        }
    }
}

#[derive(Default)]
pub struct SkinningCache {
    pub inverse_bind_matrices: Vec<Mat4>,
    pub skin_data: Vec<GpuSkinData>,
    pub entity_skin_indices: HashMap<Entity, u32>,
    pub joint_entities: Vec<Entity>,
    pub total_joints: u32,
    pub skinned_entity_ids: Vec<u32>,
    pub static_data_uploaded: bool,
}

impl SkinningCache {
    pub fn needs_rebuild(&self, world: &World) -> bool {
        if !self.static_data_uploaded {
            return true;
        }

        let current_entities: Vec<Entity> = world.core.query_entities(SKIN | RENDER_MESH).collect();

        if current_entities.len() != self.skinned_entity_ids.len() {
            return true;
        }

        for (index, entity) in current_entities.iter().enumerate() {
            if index >= self.skinned_entity_ids.len() || entity.id != self.skinned_entity_ids[index]
            {
                return true;
            }
        }

        false
    }

    pub fn rebuild_static_data(&mut self, world: &World) {
        let skinned_entities: Vec<Entity> = world.core.query_entities(SKIN | RENDER_MESH).collect();

        self.skinned_entity_ids = skinned_entities.iter().map(|e| e.id).collect();
        self.inverse_bind_matrices.clear();
        self.skin_data.clear();
        self.entity_skin_indices.clear();
        self.joint_entities.clear();
        self.total_joints = 0;

        if skinned_entities.is_empty() {
            self.static_data_uploaded = true;
            return;
        }

        let mut bone_entity_to_index: HashMap<Entity, u32> = HashMap::new();
        let mut bone_index = 0u32;

        for &entity in &skinned_entities {
            if let Some(skin) = world.core.get_skin(entity) {
                for &joint_entity in &skin.joints {
                    bone_entity_to_index.entry(joint_entity).or_insert_with(|| {
                        let index = bone_index;
                        self.joint_entities.push(joint_entity);
                        bone_index += 1;
                        index
                    });
                }
            }
        }

        let mut skin_configs: HashMap<u64, u32> = HashMap::new();
        let mut unique_skin_index = 0u32;
        let mut current_ibm_offset = 0u32;
        let mut current_output_offset = 0u32;

        for &entity in &skinned_entities {
            if let Some(skin) = world.core.get_skin(entity) {
                let mut hasher = std::collections::hash_map::DefaultHasher::new();
                for joint in skin.joints.iter() {
                    use std::hash::Hash;
                    joint.id.hash(&mut hasher);
                }
                for matrix in skin.inverse_bind_matrices.iter() {
                    for row in 0..4 {
                        for column in 0..4 {
                            use std::hash::Hash;
                            matrix[(row, column)].to_bits().hash(&mut hasher);
                        }
                    }
                }
                use std::hash::Hasher;
                let skin_hash = hasher.finish();

                let skin_index = if let Some(&existing_index) = skin_configs.get(&skin_hash) {
                    existing_index
                } else {
                    let index = unique_skin_index;
                    skin_configs.insert(skin_hash, index);

                    let base_bone_index = if let Some(&first_joint) = skin.joints.first() {
                        bone_entity_to_index.get(&first_joint).copied().unwrap_or(0)
                    } else {
                        0
                    };

                    let joint_count = skin.joints.len().min(256) as u32;

                    let gpu_skin_data = GpuSkinData {
                        joint_count,
                        base_bone_index,
                        base_ibm_index: current_ibm_offset,
                        base_output_index: current_output_offset,
                    };
                    self.skin_data.push(gpu_skin_data);

                    for (joint_index, &inverse_bind) in
                        skin.inverse_bind_matrices.iter().enumerate()
                    {
                        if joint_index < 256 {
                            self.inverse_bind_matrices.push(inverse_bind);
                        }
                    }

                    current_ibm_offset += joint_count;
                    current_output_offset += joint_count;

                    unique_skin_index += 1;
                    index
                };

                self.entity_skin_indices.insert(entity, skin_index);
            }
        }

        self.total_joints = current_output_offset;
        self.static_data_uploaded = true;
    }

    pub fn collect_bone_transforms(&self, world: &World) -> Vec<Mat4> {
        let mut bone_transforms = Vec::with_capacity(self.joint_entities.len());

        for &joint_entity in &self.joint_entities {
            if let Some(joint_global_transform) = world.core.get_global_transform(joint_entity) {
                bone_transforms.push(joint_global_transform.0);
            } else {
                bone_transforms.push(Mat4::identity());
            }
        }

        bone_transforms
    }

    pub fn get_joint_offset(&self, skin_index: u32) -> u32 {
        if let Some(skin_data) = self.skin_data.get(skin_index as usize) {
            skin_data.base_output_index
        } else {
            0
        }
    }
}

pub fn collect_skinning_data(world: &World) -> SkinningUploadData {
    let skinned_entities: Vec<Entity> = world.core.query_entities(SKIN | RENDER_MESH).collect();

    if skinned_entities.is_empty() {
        return SkinningUploadData {
            bone_transforms: Vec::new(),
            inverse_bind_matrices: Vec::new(),
            skin_data: Vec::new(),
            entity_skin_indices: HashMap::new(),
            total_joints: 0,
        };
    }

    let mut bone_transforms = Vec::new();
    let mut inverse_bind_matrices = Vec::new();
    let mut skin_data_vec = Vec::new();
    let mut entity_skin_indices = HashMap::new();

    let mut bone_entity_to_index: HashMap<Entity, u32> = HashMap::new();
    let mut bone_index = 0u32;

    for &entity in &skinned_entities {
        if let Some(skin) = world.core.get_skin(entity) {
            for &joint_entity in &skin.joints {
                bone_entity_to_index.entry(joint_entity).or_insert_with(|| {
                    let index = bone_index;

                    if let Some(joint_global_transform) =
                        world.core.get_global_transform(joint_entity)
                    {
                        bone_transforms.push(joint_global_transform.0);
                    } else {
                        bone_transforms.push(Mat4::identity());
                    }

                    bone_index += 1;
                    index
                });
            }
        }
    }

    let mut skin_configs: HashMap<u64, u32> = HashMap::new();
    let mut unique_skin_index = 0u32;
    let mut current_ibm_offset = 0u32;
    let mut current_output_offset = 0u32;

    for &entity in &skinned_entities {
        if let Some(skin) = world.core.get_skin(entity) {
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
            for joint in skin.joints.iter() {
                use std::hash::Hash;
                joint.id.hash(&mut hasher);
            }
            for matrix in skin.inverse_bind_matrices.iter() {
                for row in 0..4 {
                    for column in 0..4 {
                        use std::hash::Hash;
                        matrix[(row, column)].to_bits().hash(&mut hasher);
                    }
                }
            }
            use std::hash::Hasher;
            let skin_hash = hasher.finish();

            let skin_index = if let Some(&existing_index) = skin_configs.get(&skin_hash) {
                existing_index
            } else {
                let index = unique_skin_index;
                skin_configs.insert(skin_hash, index);

                let base_bone_index = if let Some(&first_joint) = skin.joints.first() {
                    bone_entity_to_index.get(&first_joint).copied().unwrap_or(0)
                } else {
                    0
                };

                let joint_count = skin.joints.len().min(256) as u32;

                let gpu_skin_data = GpuSkinData {
                    joint_count,
                    base_bone_index,
                    base_ibm_index: current_ibm_offset,
                    base_output_index: current_output_offset,
                };
                skin_data_vec.push(gpu_skin_data);

                for (joint_index, &inverse_bind) in skin.inverse_bind_matrices.iter().enumerate() {
                    if joint_index < 256 {
                        inverse_bind_matrices.push(inverse_bind);
                    }
                }

                current_ibm_offset += joint_count;
                current_output_offset += joint_count;

                unique_skin_index += 1;
                index
            };

            entity_skin_indices.insert(entity, skin_index);
        }
    }

    SkinningUploadData {
        bone_transforms,
        inverse_bind_matrices,
        skin_data: skin_data_vec,
        entity_skin_indices,
        total_joints: current_output_offset,
    }
}