nightshade 0.43.0

A cross-platform data-oriented game engine.
Documentation
//! Cloth mesh synchronization.
//!
//! Runs in the default frame schedule. Diffs every entity's [`Cloth`]
//! component against a snapshot and, on change, rebuilds the rest-pose
//! grid mesh in the mesh cache and the entity's inflated bounding volume.
//! The renderer picks up cache changes automatically, so resizing or
//! re-tiling a cloth at runtime just works.

use crate::ecs::bounding_volume::components::{BoundingVolume, OrientedBoundingBox};
use crate::ecs::cloth::components::Cloth;
use crate::ecs::mesh::components::{Mesh, Vertex};
use crate::ecs::prefab::resources::mesh_cache_insert;
use crate::ecs::world::{CLOTH, RENDER_MESH, World};
use nalgebra_glm::{Quat, Vec3};

/// Re-registers grid meshes and bounding volumes for changed [`Cloth`]
/// components. Part of the default frame schedule.
pub fn sync_cloth_meshes_system(world: &mut World) {
    let mut changed: Vec<(freecs::Entity, Cloth, String)> = Vec::new();
    let mut seen: Vec<freecs::Entity> = Vec::new();
    world
        .core
        .query()
        .with(CLOTH | RENDER_MESH)
        .iter(|entity, table, index| {
            seen.push(entity);
            let cloth = &table.cloth[index];
            let snapshot = world.resources.cloth_sync.snapshots.get(&entity);
            if snapshot != Some(cloth) {
                changed.push((entity, cloth.clone(), table.render_mesh[index].name.clone()));
            }
        });

    world
        .resources
        .cloth_sync
        .snapshots
        .retain(|entity, _| seen.contains(entity));

    for (entity, cloth, mesh_name) in changed {
        let mesh = build_cloth_mesh(&cloth);
        mesh_cache_insert(&mut world.resources.assets.mesh_cache, mesh_name, mesh);
        world
            .core
            .set_bounding_volume(entity, cloth_bounding_volume(&cloth));
        world.resources.cloth_sync.snapshots.insert(entity, cloth);
    }
}

/// Builds the rest-pose grid mesh for a cloth. Vertices are laid out in
/// row-major particle order so the simulation's vertex writeback indexes
/// line up one to one.
pub fn build_cloth_mesh(cloth: &Cloth) -> Mesh {
    let columns = cloth.columns.max(2);
    let rows = cloth.rows.max(2);
    let spacing_x = cloth.width / (columns - 1) as f32;
    let spacing_y = cloth.height / (rows - 1) as f32;

    let mut vertices = Vec::with_capacity((columns * rows) as usize);
    for row in 0..rows {
        for column in 0..columns {
            vertices.push(Vertex {
                position: [
                    -cloth.width / 2.0 + column as f32 * spacing_x,
                    -(row as f32 * spacing_y),
                    0.0,
                ],
                normal: [0.0, 0.0, 1.0],
                tex_coords: [
                    column as f32 / (columns - 1) as f32 * cloth.texture_tiling.x,
                    row as f32 / (rows - 1) as f32 * cloth.texture_tiling.y,
                ],
                tex_coords_1: [0.0, 0.0],
                tangent: [1.0, 0.0, 0.0, 1.0],
                color: [1.0, 1.0, 1.0, 1.0],
            });
        }
    }

    let mut indices = Vec::with_capacity(((columns - 1) * (rows - 1) * 6) as usize);
    for row in 0..rows - 1 {
        for column in 0..columns - 1 {
            let top_left = row * columns + column;
            let top_right = top_left + 1;
            let bottom_left = top_left + columns;
            let bottom_right = bottom_left + 1;
            indices.extend_from_slice(&[
                top_left,
                bottom_left,
                top_right,
                top_right,
                bottom_left,
                bottom_right,
            ]);
        }
    }

    Mesh {
        vertices,
        indices,
        bounding_volume: Some(cloth_bounding_volume(cloth)),
        skin_data: None,
        morph_targets: None,
    }
}

/// Local-space bounding volume inflated to cover the simulation's reach:
/// the sheet can swing roughly its own height in any horizontal direction
/// from the anchor line.
pub fn cloth_bounding_volume(cloth: &Cloth) -> BoundingVolume {
    let half_extents = Vec3::new(
        cloth.width / 2.0 + cloth.height * 0.5,
        cloth.height / 2.0 + cloth.height * 0.1,
        cloth.height * 0.6,
    );
    BoundingVolume {
        obb: OrientedBoundingBox {
            center: Vec3::new(0.0, -cloth.height / 2.0, 0.0),
            half_extents,
            orientation: Quat::identity(),
        },
        sphere_radius: nalgebra_glm::length(&half_extents),
    }
}