nightshade-renderer 0.53.0

GPU-driven wgpu renderer with a built-in frame graph.
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;
/// Sentinel entity id the terrain writes into the R32Float pick target. The bit
/// pattern must be a finite f32 so it survives `bitcast<f32>` const-evaluation in
/// WGSL and round-trips through the float target without NaN canonicalization. It
/// stays far above any real entity id so it never collides. The WGSL copy in
/// `terrain_render.wgsl` must match.
pub const TERRAIN_PICK_ID: u32 = 0x7F7F_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: crate::entity::RenderEntity,
}

#[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
    }
}