veloren/common/src/terrain/mod.rs

400 lines
12 KiB
Rust

pub mod biome;
pub mod block;
pub mod chonk;
pub mod map;
pub mod site;
pub mod sprite;
pub mod structure;
// Reexports
pub use self::{
biome::BiomeKind,
block::{Block, BlockKind},
map::MapSizeLg,
site::SitesKind,
sprite::SpriteKind,
structure::{Structure, StructuresGroup},
};
use roots::find_roots_cubic;
use serde::{Deserialize, Serialize};
use crate::{
vol::{ReadVol, RectVolSize},
volumes::vol_grid_2d::VolGrid2d,
};
use vek::*;
// TerrainChunkSize
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerrainChunkSize;
/// Base two logarithm of the number of blocks along either horizontal axis of
/// a chunk.
///
/// NOTE: (1 << CHUNK_SIZE_LG) is guaranteed to fit in a u32.
///
/// NOTE: A lot of code assumes that the two dimensions are equal, so we make it
/// explicit here.
///
/// NOTE: It is highly unlikely that a value greater than 5 will work, as many
/// frontend optimizations rely on being able to pack chunk horizontal
/// dimensions into 5 bits each.
pub const TERRAIN_CHUNK_BLOCKS_LG: u32 = 5;
impl RectVolSize for TerrainChunkSize {
const RECT_SIZE: Vec2<u32> = Vec2 {
x: (1 << TERRAIN_CHUNK_BLOCKS_LG),
y: (1 << TERRAIN_CHUNK_BLOCKS_LG),
};
}
impl TerrainChunkSize {
#[inline(always)]
/// Convert dimensions in terms of chunks into dimensions in terms of blocks
/// ```
/// use vek::*;
/// use veloren_common::terrain::TerrainChunkSize;
///
/// assert_eq!(TerrainChunkSize::blocks(Vec2::new(3, 2)), Vec2::new(96, 64));
/// ```
pub fn blocks(chunks: Vec2<u32>) -> Vec2<u32> { chunks * Self::RECT_SIZE }
/// Calculate the world position (i.e. in blocks) at the center of this
/// chunk
/// ```
/// use vek::*;
/// use veloren_common::terrain::TerrainChunkSize;
///
/// assert_eq!(
/// TerrainChunkSize::center_wpos(Vec2::new(0, 2)),
/// Vec2::new(16, 80)
/// );
/// ```
pub fn center_wpos(chunk_pos: Vec2<i32>) -> Vec2<i32> {
chunk_pos * Self::RECT_SIZE.as_::<i32>() + Self::RECT_SIZE.as_::<i32>() / 2
}
}
// TerrainChunkMeta
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerrainChunkMeta {
name: Option<String>,
biome: BiomeKind,
alt: f32,
tree_density: f32,
contains_cave: bool,
contains_river: bool,
temp: f32,
contains_settlement: bool,
contains_dungeon: bool,
}
#[allow(clippy::too_many_arguments)]
impl TerrainChunkMeta {
pub fn new(
name: Option<String>,
biome: BiomeKind,
alt: f32,
tree_density: f32,
contains_cave: bool,
contains_river: bool,
temp: f32,
contains_settlement: bool,
contains_dungeon: bool,
) -> Self {
Self {
name,
biome,
alt,
tree_density,
contains_cave,
contains_river,
temp,
contains_settlement,
contains_dungeon,
}
}
pub fn void() -> Self {
Self {
name: None,
biome: BiomeKind::Void,
alt: 0.0,
tree_density: 0.0,
contains_cave: false,
contains_river: false,
temp: 0.0,
contains_settlement: false,
contains_dungeon: false,
}
}
pub fn name(&self) -> &str { self.name.as_deref().unwrap_or("Wilderness") }
pub fn biome(&self) -> BiomeKind { self.biome }
pub fn alt(&self) -> f32 { self.alt }
pub fn tree_density(&self) -> f32 { self.tree_density }
pub fn contains_cave(&self) -> bool { self.contains_cave }
pub fn contains_river(&self) -> bool { self.contains_river }
pub fn contains_settlement(&self) -> bool { self.contains_settlement }
pub fn contains_dungeon(&self) -> bool { self.contains_dungeon }
pub fn temp(&self) -> f32 { self.temp }
}
// Terrain type aliases
pub type TerrainChunk = chonk::Chonk<Block, TerrainChunkSize, TerrainChunkMeta>;
pub type TerrainGrid = VolGrid2d<TerrainChunk>;
impl TerrainGrid {
/// Find a location suitable for spawning an entity near the given
/// position (but in the same chunk).
pub fn find_space(&self, pos: Vec3<i32>) -> Vec3<i32> {
self.try_find_space(pos).unwrap_or(pos)
}
pub fn try_find_space(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
const SEARCH_DIST: i32 = 63;
(0..SEARCH_DIST * 2 + 1)
.map(|i| if i % 2 == 0 { i } else { -i } / 2)
.map(|z_diff| pos + Vec3::unit_z() * z_diff)
.find(|test_pos| {
self.get(test_pos - Vec3::unit_z())
.map_or(false, |b| b.is_filled())
&& (0..2).all(|z| {
self.get(test_pos + Vec3::unit_z() * z)
.map_or(true, |b| !b.is_solid())
})
})
}
}
impl TerrainChunk {
/// Find the highest or lowest accessible position within the chunk
pub fn find_accessible_pos(&self, spawn_wpos: Vec2<i32>, ascending: bool) -> Vec3<f32> {
let min_z = self.get_min_z();
let max_z = self.get_max_z();
let pos = Vec3::new(
spawn_wpos.x,
spawn_wpos.y,
if ascending { min_z } else { max_z },
);
(0..(max_z - min_z))
.map(|z_diff| {
if ascending {
pos + Vec3::unit_z() * z_diff
} else {
pos - Vec3::unit_z() * z_diff
}
})
.find(|test_pos| {
let chunk_relative_xy = test_pos
.xy()
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.rem_euclid(sz as i32));
self.get(
Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
- Vec3::unit_z(),
)
.map_or(false, |b| b.is_filled())
&& (0..3).all(|z| {
self.get(
Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
+ Vec3::unit_z() * z,
)
.map_or(true, |b| !b.is_solid())
})
})
.unwrap_or(pos)
.map(|e| e as f32)
+ 0.5
}
}
// Terrain helper functions used across multiple crates.
/// Computes the position Vec2 of a SimChunk from an index, where the index was
/// generated by uniform_noise.
///
/// NOTE: Dimensions obey constraints on [map::MapConfig::map_size_lg].
#[inline(always)]
pub fn uniform_idx_as_vec2(map_size_lg: MapSizeLg, idx: usize) -> Vec2<i32> {
let x_mask = (1 << map_size_lg.vec().x) - 1;
Vec2::new((idx & x_mask) as i32, (idx >> map_size_lg.vec().x) as i32)
}
/// Computes the index of a Vec2 of a SimChunk from a position, where the index
/// is generated by uniform_noise. NOTE: Both components of idx should be
/// in-bounds!
#[inline(always)]
pub fn vec2_as_uniform_idx(map_size_lg: MapSizeLg, idx: Vec2<i32>) -> usize {
((idx.y as usize) << map_size_lg.vec().x) | idx.x as usize
}
// NOTE: want to keep this such that the chunk index is in ascending order!
pub const NEIGHBOR_DELTA: [(i32, i32); 8] = [
(-1, -1),
(0, -1),
(1, -1),
(-1, 0),
(1, 0),
(-1, 1),
(0, 1),
(1, 1),
];
/// Iterate through all cells adjacent to a chunk.
#[inline(always)]
pub fn neighbors(map_size_lg: MapSizeLg, posi: usize) -> impl Clone + Iterator<Item = usize> {
let pos = uniform_idx_as_vec2(map_size_lg, posi);
let world_size = map_size_lg.chunks();
NEIGHBOR_DELTA
.iter()
.map(move |&(x, y)| Vec2::new(pos.x + x, pos.y + y))
.filter(move |pos| {
pos.x >= 0 && pos.y >= 0 && pos.x < world_size.x as i32 && pos.y < world_size.y as i32
})
.map(move |pos| vec2_as_uniform_idx(map_size_lg, pos))
}
pub fn river_spline_coeffs(
// _sim: &WorldSim,
chunk_pos: Vec2<f64>,
spline_derivative: Vec2<f32>,
downhill_pos: Vec2<f64>,
) -> Vec3<Vec2<f64>> {
let dxy = downhill_pos - chunk_pos;
// Since all splines have been precomputed, we don't have to do that much work
// to evaluate the spline. The spline is just ax^2 + bx + c = 0, where
//
// a = dxy - chunk.river.spline_derivative
// b = chunk.river.spline_derivative
// c = chunk_pos
let spline_derivative = spline_derivative.map(|e| e as f64);
Vec3::new(dxy - spline_derivative, spline_derivative, chunk_pos)
}
/// Find the nearest point from a quadratic spline to this point (in terms of t,
/// the "distance along the curve" by which our spline is parameterized). Note
/// that if t < 0.0 or t >= 1.0, we probably shouldn't be considered "on the
/// curve"... hopefully this works out okay and gives us what we want (a
/// river that extends outwards tangent to a quadratic curve, with width
/// configured by distance along the line).
#[allow(clippy::many_single_char_names)]
pub fn quadratic_nearest_point(
spline: &Vec3<Vec2<f64>>,
point: Vec2<f64>,
_line: Vec2<Vec2<f64>>, // Used for alternative distance functions below
) -> Option<(f64, Vec2<f64>, f64)> {
//let eval_at = |t: f64| spline.x * t * t + spline.y * t + spline.z;
// Linear
// let line = LineSegment2 {
// start: line.x,
// end: line.y,
// };
// let len_sq = line.start.distance_squared(line.end);
// let t = ((point - line.start).dot(line.end - line.start) /
// len_sq).clamped(0.0, 1.0); let pos = line.start + (line.end - line.start)
// * t; return Some((t, pos, pos.distance_squared(point)));
// Quadratic
// let curve = QuadraticBezier2 {
// start: line.x,
// ctrl: eval_at(0.5),
// end: line.y,
// };
// let (t, pos) = curve.binary_search_point_by_steps(point, 16, 0.001);
// let t = t.clamped(0.0, 1.0);
// let pos = curve.evaluate(t);
// return Some((t, pos, pos.distance_squared(point)));
// Cubic
// let ctrl_at = |t: f64, end: f64| {
// let a = eval_at(end);
// let b = eval_at(Lerp::lerp(end, t, 0.1));
// let dir = (b - a).normalized();
// let exact = eval_at(t);
// a + dir * exact.distance(a)
// };
// let curve = CubicBezier2 {
// start: line.x,
// ctrl0: ctrl_at(0.33, 0.0),
// ctrl1: ctrl_at(0.66, 1.0),
// end: line.y,
// };
// let (t, pos) = curve.binary_search_point_by_steps(point, 12, 0.01);
// let t = t.clamped(0.0, 1.0);
// let pos = curve.evaluate(t);
// return Some((t, pos, pos.distance_squared(point)));
let a = spline.z.x;
let b = spline.y.x;
let c = spline.x.x;
let d = point.x;
let e = spline.z.y;
let f = spline.y.y;
let g = spline.x.y;
let h = point.y;
// This is equivalent to solving the following cubic equation (derivation is a
// bit annoying):
//
// A = 2(c^2 + g^2)
// B = 3(b * c + g * f)
// C = ((a - d) * 2 * c + b^2 + (e - h) * 2 * g + f^2)
// D = ((a - d) * b + (e - h) * f)
//
// Ax³ + Bx² + Cx + D = 0
//
// Once solved, this yield up to three possible values for t (reflecting minimal
// and maximal values). We should choose the minimal such real value with t
// between 0.0 and 1.0. If we fall outside those bounds, then we are
// outside the spline and return None.
let a_ = (c * c + g * g) * 2.0;
let b_ = (b * c + g * f) * 3.0;
let a_d = a - d;
let e_h = e - h;
let c_ = a_d * c * 2.0 + b * b + e_h * g * 2.0 + f * f;
let d_ = a_d * b + e_h * f;
let roots = find_roots_cubic(a_, b_, c_, d_);
let roots = roots.as_ref();
let min_root = roots
.iter()
.copied()
.map(|root| {
let river_point = spline.x * root * root + spline.y * root + spline.z;
if root > 0.0 && root < 1.0 {
(root, river_point)
} else {
let root = root.clamped(0.0, 1.0);
let river_point = spline.x * root * root + spline.y * root + spline.z;
(root, river_point)
}
})
.map(|(root, river_point)| {
let river_distance = river_point.distance_squared(point);
(root, river_point, river_distance)
})
// In the (unlikely?) case that distances are equal, prefer the earliest point along the
// river.
.min_by(|&(ap, _, a), &(bp, _, b)| {
(a, !(0.0..=1.0).contains(&ap), ap)
.partial_cmp(&(b, !(0.0..=1.0).contains(&bp), bp))
.unwrap()
});
min_root
}