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 = 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) -> Vec2 { 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) -> Vec2 { chunk_pos * Self::RECT_SIZE.as_::() + Self::RECT_SIZE.as_::() / 2 } } // TerrainChunkMeta #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TerrainChunkMeta { name: Option, 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, 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; pub type TerrainGrid = VolGrid2d; 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) -> Vec3 { self.try_find_space(pos).unwrap_or(pos) } pub fn try_find_space(&self, pos: Vec3) -> Option> { 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, ascending: bool) -> Vec3 { 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 { 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) -> 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 { 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, spline_derivative: Vec2, downhill_pos: Vec2, ) -> Vec3> { 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>, point: Vec2, _line: Vec2>, // Used for alternative distance functions below ) -> Option<(f64, Vec2, 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 }