nightshade 0.52.0

A cross-platform data-oriented game engine.
Documentation
//! The terrain collider composition: heightfield collider tiles kept
//! windowed around an anchor entity. The terrain surface itself is a
//! renderer capability; this plugin owns the physics side.

#[cfg(feature = "physics")]
use crate::render::terrain_config::*;
#[cfg(feature = "physics")]
use nalgebra_glm::Vec3;

/// The marker tag naming the entity the collider window follows,
/// typically the player or the fly camera. Move it with
/// `world.add_tag_type::<TerrainColliderAnchor>(entity)` /
/// `remove_tag_type` when the followed entity changes. Without a tagged
/// entity the window stays around the origin.
pub struct TerrainColliderAnchor;

/// Maintains heightfield collider tiles around the tagged
/// [`TerrainColliderAnchor`], spawning missing tiles from the terrain
/// snapshot and retiring the ones the anchor left behind. A no-op while
/// `world.resources.terrain.enabled` is false.
#[cfg(feature = "physics")]
pub fn terrain_collider_system(world: &mut crate::ecs::world::World) {
    use crate::ecs::world::commands::spawn_entities;
    use crate::ecs::world::{COLLIDER, RIGID_BODY};
    use crate::plugins::physics::components::{
        ColliderComponent, ColliderShape, RigidBodyComponent,
    };
    use crate::plugins::physics::types::InteractionGroups;

    let settings_enabled = world.resources.terrain.enabled;
    let settings_revision = world.resources.terrain.revision;
    let collider_radius = world.resources.terrain.collider_radius;

    if world.resources.terrain_render.collider_revision != settings_revision {
        let entities: Vec<freecs::Entity> = world
            .resources
            .terrain_render
            .collider_tiles
            .values()
            .map(|tile| crate::render_driver::scene_entity(tile.entity))
            .collect();
        for entity in entities {
            world.despawn_entities(&[entity]);
        }
        world.resources.terrain_render.collider_tiles.clear();
        world.resources.terrain_render.height_tiles.clear();
        world.resources.terrain_render.collider_revision = settings_revision;
    }

    if !settings_enabled {
        return;
    }

    let center = world
        .query_tag_type::<TerrainColliderAnchor>()
        .next()
        .and_then(|entity| world.get::<crate::ecs::transform::components::LocalTransform>(entity))
        .map(|transform| transform.translation)
        .unwrap_or_else(Vec3::zeros);

    let center_tile_x = (center.x / TERRAIN_COLLIDER_TILE_METERS).floor() as i32;
    let center_tile_z = (center.z / TERRAIN_COLLIDER_TILE_METERS).floor() as i32;

    let mut missing = Vec::new();
    for offset_z in -collider_radius..=collider_radius {
        for offset_x in -collider_radius..=collider_radius {
            let coord = (center_tile_x + offset_x, center_tile_z + offset_z);
            if !world
                .resources
                .terrain_render
                .collider_tiles
                .contains_key(&coord)
            {
                missing.push(coord);
            }
        }
    }

    let share = world.resources.terrain_render.share.clone();
    let extracted: Vec<((i32, i32), Vec<f32>)> = share.with(|shared| {
        let mut extracted = Vec::new();
        let snapshot_done = matches!(
            &shared.snapshot,
            Some(snapshot)
                if matches!(snapshot.state, TerrainSnapshotState::Mapping)
                    && snapshot.map_done.load(std::sync::atomic::Ordering::Relaxed)
        );
        if snapshot_done {
            let snapshot = shared.snapshot.take().expect("snapshot present");
            if snapshot.revision == settings_revision {
                {
                    let mapped = snapshot.buffer.slice(..).get_mapped_range();
                    let texels: &[f32] = bytemuck::cast_slice(&mapped);
                    for coord in &missing {
                        if let Some(heights) = extract_tile(texels, &snapshot, *coord) {
                            extracted.push((*coord, heights));
                        }
                    }
                }
            }
            snapshot.buffer.unmap();
        }
        if !missing.is_empty()
            && shared.snapshot.is_none()
            && shared.snapshot_request.is_none()
            && extracted.len() < missing.len()
        {
            shared.snapshot_request = Some([center.x, center.z]);
        }
        extracted
    });

    for (coord, heights) in extracted {
        let entity = spawn_entities(world, RIGID_BODY | COLLIDER, 1)[0];
        let tile_center_x =
            coord.0 as f32 * TERRAIN_COLLIDER_TILE_METERS + TERRAIN_COLLIDER_TILE_METERS * 0.5;
        let tile_center_z =
            coord.1 as f32 * TERRAIN_COLLIDER_TILE_METERS + TERRAIN_COLLIDER_TILE_METERS * 0.5;
        if let Some(rigid_body) =
            world.get_mut::<crate::plugins::physics::components::RigidBodyComponent>(entity)
        {
            *rigid_body = RigidBodyComponent::new_static().with_translation(
                tile_center_x,
                0.0,
                tile_center_z,
            );
        }
        if let Some(collider) =
            world.get_mut::<crate::plugins::physics::components::ColliderComponent>(entity)
        {
            *collider = ColliderComponent {
                handle: None,
                shape: ColliderShape::HeightField {
                    nrows: TERRAIN_COLLIDER_SAMPLES,
                    ncols: TERRAIN_COLLIDER_SAMPLES,
                    heights: transpose_heights(&heights),
                    scale: [
                        TERRAIN_COLLIDER_TILE_METERS,
                        1.0,
                        TERRAIN_COLLIDER_TILE_METERS,
                    ],
                },
                friction: 0.9,
                restitution: 0.0,
                density: 0.0,
                is_sensor: false,
                collision_groups: InteractionGroups::all(),
                solver_groups: InteractionGroups::all(),
            };
        }
        world.resources.terrain_render.collider_tiles.insert(
            coord,
            TerrainColliderTile {
                entity: crate::render_driver::render_entity(entity),
            },
        );
        world
            .resources
            .terrain_render
            .height_tiles
            .insert(coord, heights);
    }

    let stale: Vec<(i32, i32)> = world
        .resources
        .terrain_render
        .collider_tiles
        .keys()
        .filter(|(tile_x, tile_z)| {
            (tile_x - center_tile_x)
                .abs()
                .max((tile_z - center_tile_z).abs())
                > collider_radius + 1
        })
        .copied()
        .collect();
    for coord in stale {
        if let Some(tile) = world.resources.terrain_render.collider_tiles.remove(&coord) {
            world.despawn_entities(&[crate::render_driver::scene_entity(tile.entity)]);
        }
        world.resources.terrain_render.height_tiles.remove(&coord);
    }
}

#[cfg(feature = "physics")]
fn extract_tile(texels: &[f32], snapshot: &TerrainSnapshot, coord: (i32, i32)) -> Option<Vec<f32>> {
    let cache_size = TERRAIN_CACHE_SIZE as i32;
    let texels_per_meter = 1.0 / snapshot.texel_size;
    let tile_origin_x =
        (coord.0 as f32 * TERRAIN_COLLIDER_TILE_METERS * texels_per_meter).round() as i32;
    let tile_origin_z =
        (coord.1 as f32 * TERRAIN_COLLIDER_TILE_METERS * texels_per_meter).round() as i32;
    let samples = TERRAIN_COLLIDER_SAMPLES as i32;
    if tile_origin_x < snapshot.origin_texels[0]
        || tile_origin_z < snapshot.origin_texels[1]
        || tile_origin_x + samples > snapshot.origin_texels[0] + cache_size
        || tile_origin_z + samples > snapshot.origin_texels[1] + cache_size
    {
        return None;
    }
    let mut heights = Vec::with_capacity((samples * samples) as usize);
    for row in 0..samples {
        for column in 0..samples {
            let texel_x = (tile_origin_x + column).rem_euclid(cache_size);
            let texel_z = (tile_origin_z + row).rem_euclid(cache_size);
            heights.push(texels[(texel_z * cache_size + texel_x) as usize]);
        }
    }
    Some(heights)
}

#[cfg(feature = "physics")]
fn transpose_heights(heights: &[f32]) -> Vec<f32> {
    let samples = TERRAIN_COLLIDER_SAMPLES;
    let mut output = vec![0.0; heights.len()];
    for row in 0..samples {
        for column in 0..samples {
            output[row + samples * column] = heights[row * samples + column];
        }
    }
    output
}

/// Registers the collider maintenance into the frame schedule's update
/// phase.
#[cfg(feature = "physics")]
pub fn install(world: &mut crate::ecs::world::World) {
    crate::schedule::schedule_push(
        &mut world.resources.schedules.frame,
        crate::schedule::FramePhase::Update,
        terrain_collider_system,
    );
}

/// Keeps terrain heightfield colliders windowed around the entity tagged
/// [`TerrainColliderAnchor`]. Enable the surface with
/// `world.resources.terrain.enabled` and tag the entity the window
/// follows; the plugin owns the per-frame tile maintenance.
#[cfg(feature = "physics")]
pub struct TerrainPlugin;

#[cfg(feature = "physics")]
impl crate::app::Plugin for TerrainPlugin {
    fn build(&self, app: &mut crate::app::App) {
        app.add_startup_system(install);
    }
}