nightshade 0.42.0

A cross-platform data-oriented game engine.
Documentation
use nalgebra_glm::Vec3;
use std::collections::HashMap;
use std::sync::Arc;

pub const TERRAIN_CACHE_SIZE: u32 = 512;
pub const TERRAIN_TILE_TEXELS: u32 = 64;
pub const TERRAIN_GRASS_LEVEL: u32 = 1;
pub const TERRAIN_COLLIDER_LAYER: u32 = 10;
pub const TERRAIN_PICK_ID: u32 = 0xFFFF_FFF0;
pub const TERRAIN_COLLIDER_TILE_METERS: f32 = 64.0;
pub const TERRAIN_COLLIDER_SAMPLES: usize = 65;

#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct TerrainSettings {
    pub enabled: bool,
    pub seed: u32,
    pub texel_size: f32,
    pub level_count: u32,
    pub continental_spline: [[f32; 2]; 8],
    pub erosion_spline: [[f32; 2]; 8],
    pub continental_frequency: f32,
    pub erosion_frequency: f32,
    pub ridge_frequency: f32,
    pub warp_frequency: f32,
    pub warp_strength: f32,
    pub detail_amplitude: f32,
    pub erosion_sharpness: f32,
    pub height_min: f32,
    pub height_max: f32,
    pub rock_slope: f32,
    pub snow_height: f32,
    pub rock_color: [f32; 4],
    pub snow_color: [f32; 4],
    pub origin_flatten_radius: f32,
    pub collider_radius: i32,
    pub revision: u64,
}

impl Default for TerrainSettings {
    fn default() -> Self {
        Self {
            enabled: false,
            seed: 7,
            texel_size: 0.5,
            level_count: 10,
            continental_spline: [
                [-1.0, -30.0],
                [-0.4, -8.0],
                [-0.1, 0.0],
                [0.25, 2.0],
                [0.5, 14.0],
                [0.7, 40.0],
                [0.85, 90.0],
                [1.0, 140.0],
            ],
            erosion_spline: [
                [-1.0, 0.02],
                [-0.5, 0.08],
                [-0.1, 0.2],
                [0.2, 0.45],
                [0.45, 0.7],
                [0.65, 1.0],
                [0.85, 1.0],
                [1.0, 1.0],
            ],
            continental_frequency: 1.0 / 4000.0,
            erosion_frequency: 1.0 / 1400.0,
            ridge_frequency: 1.0 / 320.0,
            warp_frequency: 1.0 / 600.0,
            warp_strength: 120.0,
            detail_amplitude: 1.4,
            erosion_sharpness: 1.25,
            height_min: -60.0,
            height_max: 320.0,
            rock_slope: 0.55,
            snow_height: 120.0,
            rock_color: [0.16, 0.13, 0.11, 1.0],
            snow_color: [0.75, 0.78, 0.85, 1.0],
            origin_flatten_radius: 0.0,
            collider_radius: 2,
            revision: 1,
        }
    }
}

pub enum TerrainSnapshotState {
    Requested,
    Encoded,
    Mapping,
}

pub struct TerrainSnapshot {
    pub buffer: wgpu::Buffer,
    pub map_done: Arc<std::sync::atomic::AtomicBool>,
    pub origin_texels: [i32; 2],
    pub texel_size: f32,
    pub revision: u64,
    pub state: TerrainSnapshotState,
}

#[derive(Default)]
pub struct TerrainShared {
    pub cache_ready: bool,
    pub cache_view: Option<wgpu::TextureView>,
    pub grass_origin_texels: [i32; 2],
    pub grass_texel_size: f32,
    pub snapshot_request: Option<[f32; 2]>,
    pub snapshot: Option<TerrainSnapshot>,
}

#[derive(Clone)]
pub struct TerrainShare {
    #[cfg(not(target_arch = "wasm32"))]
    inner: Arc<std::sync::Mutex<TerrainShared>>,
    #[cfg(target_arch = "wasm32")]
    inner: std::rc::Rc<std::cell::RefCell<TerrainShared>>,
}

impl Default for TerrainShare {
    fn default() -> Self {
        Self {
            #[cfg(not(target_arch = "wasm32"))]
            inner: Arc::new(std::sync::Mutex::new(TerrainShared::default())),
            #[cfg(target_arch = "wasm32")]
            inner: std::rc::Rc::new(std::cell::RefCell::new(TerrainShared::default())),
        }
    }
}

impl TerrainShare {
    pub fn with<Output>(&self, action: impl FnOnce(&mut TerrainShared) -> Output) -> Output {
        #[cfg(not(target_arch = "wasm32"))]
        {
            action(&mut self.inner.lock().expect("terrain share poisoned"))
        }
        #[cfg(target_arch = "wasm32")]
        {
            action(&mut self.inner.borrow_mut())
        }
    }
}

pub struct TerrainColliderTile {
    pub entity: freecs::Entity,
}

#[derive(Default)]
pub struct TerrainRenderState {
    pub share: TerrainShare,
    pub collider_tiles: HashMap<(i32, i32), TerrainColliderTile>,
    pub collider_revision: u64,
    pub height_tiles: HashMap<(i32, i32), Vec<f32>>,
}

impl TerrainRenderState {
    pub fn sample_height(&self, x: f32, z: f32) -> Option<f32> {
        let tile_x = (x / TERRAIN_COLLIDER_TILE_METERS).floor() as i32;
        let tile_z = (z / TERRAIN_COLLIDER_TILE_METERS).floor() as i32;
        let heights = self.height_tiles.get(&(tile_x, tile_z))?;
        let local_x = x - tile_x as f32 * TERRAIN_COLLIDER_TILE_METERS;
        let local_z = z - tile_z as f32 * TERRAIN_COLLIDER_TILE_METERS;
        let sample_x = local_x.clamp(0.0, TERRAIN_COLLIDER_TILE_METERS - 0.001);
        let sample_z = local_z.clamp(0.0, TERRAIN_COLLIDER_TILE_METERS - 0.001);
        let column = sample_x.floor() as usize;
        let row = sample_z.floor() as usize;
        let fraction_x = sample_x - column as f32;
        let fraction_z = sample_z - row as f32;
        let stride = TERRAIN_COLLIDER_SAMPLES;
        let h00 = heights[row * stride + column];
        let h10 = heights[row * stride + column + 1];
        let h01 = heights[(row + 1) * stride + column];
        let h11 = heights[(row + 1) * stride + column + 1];
        let x0 = h00 + (h10 - h00) * fraction_x;
        let x1 = h01 + (h11 - h01) * fraction_x;
        Some(x0 + (x1 - x0) * fraction_z)
    }

    pub fn collider_ready(&self, center: Vec3, radius: i32) -> bool {
        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;
        for offset_z in -radius..=radius {
            for offset_x in -radius..=radius {
                let coord = (center_tile_x + offset_x, center_tile_z + offset_z);
                if !self.collider_tiles.contains_key(&coord) {
                    return false;
                }
            }
        }
        true
    }
}

#[cfg(feature = "physics")]
pub fn terrain_collider_system(world: &mut crate::ecs::world::World, center: Vec3) {
    use crate::ecs::physics::components::{ColliderComponent, ColliderShape, RigidBodyComponent};
    use crate::ecs::physics::types::InteractionGroups;
    use crate::ecs::world::commands::spawn_entities;
    use crate::ecs::world::{COLLIDER, RIGID_BODY};

    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| 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_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.core.get_rigid_body_mut(entity) {
            *rigid_body = RigidBodyComponent::new_static().with_translation(
                tile_center_x,
                0.0,
                tile_center_z,
            );
        }
        if let Some(collider) = world.core.get_collider_mut(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 });
        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(&[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
}