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
}