nightshade 0.14.0

A cross-platform data-oriented game engine.
Documentation
pub use crate::ecs::transform::commands::*;
pub use crate::ecs::transform::queries::*;

use crate::ecs::world::{
    GLOBAL_TRANSFORM, LOCAL_TRANSFORM, LOCAL_TRANSFORM_DIRTY, PARENT, World,
    components::{GlobalTransform, Parent},
};

use crate::prelude::*;
pub fn run_systems(world: &mut World) {
    let _span = tracing::info_span!("transform_systems").entered();
    rebuild_children_cache_if_needed(world);
    update_global_transforms_system(world);
}

fn rebuild_children_cache_if_needed(world: &mut World) {
    if !world.resources.transform_state.children_cache_valid {
        validate_and_rebuild_children_cache(world);
    }
}

pub fn assign_local_transform(
    world: &mut World,
    entity: freecs::Entity,
    local_transform: crate::ecs::transform::components::LocalTransform,
) {
    world.core.set_local_transform(entity, local_transform);
    mark_local_transform_dirty(world, entity);
}

pub fn mutate_local_transform(
    world: &mut World,
    entity: freecs::Entity,
) -> Option<&mut crate::ecs::transform::components::LocalTransform> {
    mark_local_transform_dirty(world, entity);
    world.core.get_local_transform_mut(entity)
}

pub fn update_parent(world: &mut World, child: freecs::Entity, new_parent: Option<Parent>) {
    let old_parent_entity = world.core.get_parent(child).and_then(|p| p.0);

    if let Some(old_parent_entity) = old_parent_entity
        && let Some(children) = world
            .resources
            .transform_state
            .children_cache
            .get_mut(&old_parent_entity)
    {
        children.retain(|&e| e != child);
    }

    if let Some(new_parent) = new_parent {
        world.core.set_parent(child, new_parent);
        if let Some(parent_entity) = new_parent.0 {
            world
                .resources
                .transform_state
                .children_cache
                .entry(parent_entity)
                .or_default()
                .push(child);
        }
    } else {
        world.core.remove_parent(child);
    }

    world.resources.transform_state.children_cache_valid = false;
    mark_local_transform_dirty(world, child);
}

pub fn validate_and_rebuild_children_cache(world: &mut World) {
    world.resources.transform_state.children_cache.clear();

    let children_cache = &mut world.resources.transform_state.children_cache;
    world.core.query().with(PARENT).iter(|child, table, idx| {
        if let Some(parent_entity) = table.parent[idx].0 {
            children_cache.entry(parent_entity).or_default().push(child);
        }
    });

    for children in world.resources.transform_state.children_cache.values_mut() {
        children.sort_by_key(|entity| entity.id);
    }

    world.resources.transform_state.children_cache_valid = true;
}

pub fn update_global_transforms_system(world: &mut World) {
    let mut dirty_entities = std::mem::take(&mut world.resources.transform_state.dirty_entities);

    {
        let dirty_ref = &mut dirty_entities;
        world
            .core
            .query()
            .with(LOCAL_TRANSFORM_DIRTY | LOCAL_TRANSFORM | GLOBAL_TRANSFORM)
            .iter(|entity, _, _| {
                dirty_ref.push(entity);
            });
    }
    for entity in &dirty_entities {
        world.core.remove_local_transform_dirty(*entity);
    }

    dirty_entities.sort_unstable_by_key(|entity| (entity.id, entity.generation));
    dirty_entities.dedup();

    let _span = tracing::info_span!("transforms", dirty_count = dirty_entities.len()).entered();

    let mut visited = std::collections::HashSet::new();
    let mut render_dirty_entities = Vec::with_capacity(dirty_entities.len());

    for entity in dirty_entities.iter().copied() {
        let new_global_transform = if world.core.entity_has_components(entity, PARENT) {
            visited.clear();
            global_transform_of_with_cycle_detection(entity, world, &mut visited)
        } else {
            match world.core.get_local_transform(entity) {
                Some(local_transform) => local_transform.as_matrix(),
                None => nalgebra_glm::Mat4::identity(),
            }
        };

        if let Some(global_transform) = world.core.get_global_transform_mut(entity) {
            *global_transform = GlobalTransform(new_global_transform);
        }

        if world
            .core
            .entity_has_components(entity, crate::ecs::world::RENDER_MESH)
            || world
                .core
                .entity_has_components(entity, crate::ecs::world::INSTANCED_MESH)
        {
            render_dirty_entities.push(entity);
        }
    }

    for entity in render_dirty_entities {
        world
            .resources
            .mesh_render_state
            .mark_transform_dirty(entity);
    }

    dirty_entities.clear();
    world.resources.transform_state.dirty_entities = dirty_entities;
}

const MAX_HIERARCHY_DEPTH: usize = 256;

fn global_transform_of_with_cycle_detection(
    entity: freecs::Entity,
    world: &World,
    visited: &mut std::collections::HashSet<freecs::Entity>,
) -> nalgebra_glm::Mat4 {
    if visited.contains(&entity) {
        tracing::error!("Cycle detected in parent hierarchy for entity {:?}", entity);
        return nalgebra_glm::Mat4::identity();
    }

    visited.insert(entity);

    if visited.len() > MAX_HIERARCHY_DEPTH {
        tracing::warn!(
            "Transform hierarchy depth {} exceeds recommended maximum {} for entity {:?}",
            visited.len(),
            MAX_HIERARCHY_DEPTH,
            entity
        );
    }

    let Some(local_transform) = world.core.get_local_transform(entity) else {
        return nalgebra_glm::Mat4::identity();
    };

    if let Some(parent) = world.core.get_parent(entity)
        && let Some(parent_entity) = parent.0
    {
        let mut parent_transform =
            global_transform_of_with_cycle_detection(parent_entity, world, visited);

        if world.core.entity_has_ignore_parent_scale(entity) {
            parent_transform = remove_scale_from_matrix(parent_transform);
        }

        parent_transform * local_transform.as_matrix()
    } else {
        local_transform.as_matrix()
    }
}

fn remove_scale_from_matrix(matrix: nalgebra_glm::Mat4) -> nalgebra_glm::Mat4 {
    let mut result = matrix;

    let right = nalgebra_glm::vec3(matrix[(0, 0)], matrix[(1, 0)], matrix[(2, 0)]);
    let up = nalgebra_glm::vec3(matrix[(0, 1)], matrix[(1, 1)], matrix[(2, 1)]);
    let forward = nalgebra_glm::vec3(matrix[(0, 2)], matrix[(1, 2)], matrix[(2, 2)]);

    let right_normalized = nalgebra_glm::normalize(&right);
    let up_normalized = nalgebra_glm::normalize(&up);
    let forward_normalized = nalgebra_glm::normalize(&forward);

    result[(0, 0)] = right_normalized.x;
    result[(1, 0)] = right_normalized.y;
    result[(2, 0)] = right_normalized.z;

    result[(0, 1)] = up_normalized.x;
    result[(1, 1)] = up_normalized.y;
    result[(2, 1)] = up_normalized.z;

    result[(0, 2)] = forward_normalized.x;
    result[(1, 2)] = forward_normalized.y;
    result[(2, 2)] = forward_normalized.z;

    result
}