diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da798c4f4..5b321e64f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allowed collecting nearby blocks without aiming at them - Made voxygen wait until singleplayer server is initialized before attempting to connect, removing the chance for it to give up on connecting if the server takes a while to start - Log where userdata folder is located +- Switched to a Whittaker map for better tree spawning patterns +- Switched to procedural snow cover on trees +- Significantly improved terrain generation performance ### Removed diff --git a/assets/voxygen/element/map/dungeon.png b/assets/voxygen/element/map/dungeon.png new file mode 100644 index 0000000000..b4a78b954a Binary files /dev/null and b/assets/voxygen/element/map/dungeon.png differ diff --git a/assets/voxygen/element/map/town.png b/assets/voxygen/element/map/town.png new file mode 100644 index 0000000000..a20bdf9fd0 Binary files /dev/null and b/assets/voxygen/element/map/town.png differ diff --git a/assets/voxygen/item_image_manifest.ron b/assets/voxygen/item_image_manifest.ron index 8646e88f5a..d461084197 100644 --- a/assets/voxygen/item_image_manifest.ron +++ b/assets/voxygen/item_image_manifest.ron @@ -1264,7 +1264,7 @@ ), Consumable("SunflowerTea"): Png( "element.icons.item_sunflower_tea", - ), + ), // Throwables Throwable(Bomb): VoxTrans( "voxel.object.bomb", @@ -1344,7 +1344,7 @@ "voxel.object.potion_empty", (0.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 0.8, ), - // Gliders + // Gliders Glider("Starter"): VoxTrans( "voxel.glider.glider_starter", (-2.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 0.9, diff --git a/assets/voxygen/shaders/include/cloud/regular.glsl b/assets/voxygen/shaders/include/cloud/regular.glsl index 1b3b592c75..a53fb17795 100644 --- a/assets/voxygen/shaders/include/cloud/regular.glsl +++ b/assets/voxygen/shaders/include/cloud/regular.glsl @@ -132,7 +132,7 @@ vec3 get_cloud_color(vec3 surf_color, vec3 dir, vec3 origin, const float time_of float ndist = step_to_dist(trunc(dist_to_step(cdist - 0.25))); vec3 sample = cloud_at(origin + (dir + dir_diff / ndist) * ndist * splay, ndist); - vec2 density_integrals = sample.yz * (cdist - ndist); + vec2 density_integrals = max(sample.yz, vec2(0)) * (cdist - ndist); float sun_access = sample.x; float scatter_factor = 1.0 - 1.0 / (1.0 + density_integrals.x); diff --git a/assets/world/manifests/snow_pines.ron b/assets/world/manifests/snow_pines.ron deleted file mode 100644 index a70cdad690..0000000000 --- a/assets/world/manifests/snow_pines.ron +++ /dev/null @@ -1,36 +0,0 @@ -#![enable(unwrap_newtypes)] - -[ - ( - specifier: "world.tree.snow_pine.1", - center: (15, 15, 14) - ), - ( - specifier: "world.tree.snow_pine.2", - center: (15, 15, 14) - ), - ( - specifier: "world.tree.snow_pine.3", - center: (17, 15, 12) - ), - ( - specifier: "world.tree.snow_pine.4", - center: (10, 8, 12) - ), - ( - specifier: "world.tree.snow_pine.5", - center: (12, 12, 12) - ), - ( - specifier: "world.tree.snow_pine.6", - center: (11, 10, 12) - ), - ( - specifier: "world.tree.snow_pine.7", - center: (16, 15, 12) - ), - ( - specifier: "world.tree.snow_pine.8", - center: (12, 10, 12) - ), -] diff --git a/assets/world/tree/snow_pine/1.vox b/assets/world/tree/snow_pine/1.vox deleted file mode 100644 index 67057c11f5..0000000000 Binary files a/assets/world/tree/snow_pine/1.vox and /dev/null differ diff --git a/assets/world/tree/snow_pine/2.vox b/assets/world/tree/snow_pine/2.vox deleted file mode 100644 index 12d259ef95..0000000000 Binary files a/assets/world/tree/snow_pine/2.vox and /dev/null differ diff --git a/assets/world/tree/snow_pine/3.vox b/assets/world/tree/snow_pine/3.vox deleted file mode 100644 index bbda6299d8..0000000000 Binary files a/assets/world/tree/snow_pine/3.vox and /dev/null differ diff --git a/assets/world/tree/snow_pine/4.vox b/assets/world/tree/snow_pine/4.vox deleted file mode 100644 index 51a06177b9..0000000000 Binary files a/assets/world/tree/snow_pine/4.vox and /dev/null differ diff --git a/assets/world/tree/snow_pine/5.vox b/assets/world/tree/snow_pine/5.vox deleted file mode 100644 index 4fe9caa970..0000000000 Binary files a/assets/world/tree/snow_pine/5.vox and /dev/null differ diff --git a/assets/world/tree/snow_pine/6.vox b/assets/world/tree/snow_pine/6.vox deleted file mode 100644 index e69378345f..0000000000 Binary files a/assets/world/tree/snow_pine/6.vox and /dev/null differ diff --git a/assets/world/tree/snow_pine/7.vox b/assets/world/tree/snow_pine/7.vox deleted file mode 100644 index 427b9646e3..0000000000 Binary files a/assets/world/tree/snow_pine/7.vox and /dev/null differ diff --git a/assets/world/tree/snow_pine/8.vox b/assets/world/tree/snow_pine/8.vox deleted file mode 100644 index b2144f2528..0000000000 Binary files a/assets/world/tree/snow_pine/8.vox and /dev/null differ diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 2758d3f42c..494ee2c0ad 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -34,6 +34,7 @@ make_case_elim!( WeakRock = 0x11, // Explodable // 0x12 <= x < 0x20 is reserved for future rocks Grass = 0x20, // Note: *not* the same as grass sprites + Snow = 0x21, // 0x21 <= x < 0x30 is reserved for future grasses Earth = 0x30, Sand = 0x31, diff --git a/server/src/lib.rs b/server/src/lib.rs index c1f98707b8..79980fbc18 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -281,7 +281,7 @@ impl Server { .expect(&format!("no z_cache found for chunk: {}", spawn_chunk)); // get the minimum and maximum z values at which there could be solid blocks - let (min_z, _, max_z) = z_cache.get_z_limits(&mut block_sampler, index); + let (min_z, max_z) = z_cache.get_z_limits(); // round range outwards, so no potential air block is missed let min_z = min_z.floor() as i32; let max_z = max_z.ceil() as i32; @@ -296,8 +296,6 @@ impl Server { .get_with_z_cache( Vec3::new(spawn_location.x, spawn_location.y, *z), Some(&z_cache), - false, - index, ) .map(|b| b.is_air()) .unwrap_or(false) diff --git a/world/src/all.rs b/world/src/all.rs index 5c9e4fbe4c..d3a37c27dc 100644 --- a/world/src/all.rs +++ b/world/src/all.rs @@ -4,6 +4,6 @@ pub enum ForestKind { Savannah, Oak, Pine, - SnowPine, + Birch, Mangrove, } diff --git a/world/src/block/mod.rs b/world/src/block/mod.rs index 0896dd7f79..53ddc1cc73 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -1,16 +1,11 @@ -mod natural; - use crate::{ column::{ColumnGen, ColumnSample}, util::{RandomField, Sampler, SmallCache}, IndexRef, }; -use common::{ - terrain::{ - structure::{self, StructureBlock}, - Block, BlockKind, SpriteKind, Structure, - }, - vol::ReadVol, +use common::terrain::{ + structure::{self, StructureBlock}, + Block, BlockKind, SpriteKind, }; use core::ops::{Div, Mul, Range}; use serde::Deserialize; @@ -18,7 +13,6 @@ use vek::*; #[derive(Deserialize)] pub struct Colors { - pub pyramid: (u8, u8, u8), // TODO(@Sharp): After the merge, construct enough infrastructure to make it convenient to // define mapping functions over the input; i.e. we should be able to interpret some fields as // defining App, Arg>, where Fun : (Context, Arg) → (S, Type). @@ -26,17 +20,11 @@ pub struct Colors { } pub struct BlockGen<'a> { - pub column_cache: SmallCache>>, pub column_gen: ColumnGen<'a>, } impl<'a> BlockGen<'a> { - pub fn new(column_gen: ColumnGen<'a>) -> Self { - Self { - column_cache: SmallCache::default(), - column_gen, - } - } + pub fn new(column_gen: ColumnGen<'a>) -> Self { Self { column_gen } } pub fn sample_column<'b>( column_gen: &ColumnGen<'a>, @@ -49,119 +37,17 @@ impl<'a> BlockGen<'a> { .as_ref() } - pub fn get_cliff_height( - column_gen: &ColumnGen<'a>, - cache: &mut SmallCache>>, - wpos: Vec2, - close_cliffs: &[(Vec2, u32); 9], - cliff_hill: f32, - tolerance: f32, - index: IndexRef<'a>, - ) -> f32 { - close_cliffs.iter().fold( - 0.0f32, - |max_height, (cliff_pos, seed)| match Self::sample_column( - column_gen, cache, *cliff_pos, index, - ) { - Some(cliff_sample) if cliff_sample.is_cliffs && cliff_sample.spawn_rate > 0.5 => { - let cliff_pos3d = Vec3::from(*cliff_pos); - - // Conservative range of height: [15.70, 49.33] - let height = (RandomField::new(seed + 1).get(cliff_pos3d) % 64) as f32 - // [0, 63] / (1 + 3 * [0.12, 1.32]) + 3 = - // [0, 63] / (1 + [0.36, 3.96]) + 3 = - // [0, 63] / [1.36, 4.96] + 3 = - // [0, 63] / [1.36, 4.96] + 3 = - // (height min) [0, 0] + 3 = [3, 3] - // (height max) [12.70, 46.33] + 3 = [15.70, 49.33] - / (1.0 + 3.0 * cliff_sample.chaos) - + 3.0; - // Conservative range of radius: [8, 47] - let radius = RandomField::new(seed + 2).get(cliff_pos3d) % 48 + 8; - - if cliff_sample - .water_dist - .map(|d| d > radius as f32) - .unwrap_or(true) - { - max_height.max( - if cliff_pos.map(|e| e as f32).distance_squared(wpos) - < (radius as f32 + tolerance).powf(2.0) - { - cliff_sample.alt + height * (1.0 - cliff_sample.chaos) + cliff_hill - } else { - 0.0 - }, - ) - } else { - max_height - } - }, - _ => max_height, - }, - ) - } - pub fn get_z_cache(&mut self, wpos: Vec2, index: IndexRef<'a>) -> Option> { - let BlockGen { - column_cache, - column_gen, - } = self; + let BlockGen { column_gen } = self; // Main sample let sample = column_gen.get((wpos, index))?; - // Tree samples - let mut structures = [None, None, None, None, None, None, None, None, None]; - sample - .close_structures - .iter() - .zip(structures.iter_mut()) - .for_each(|(close_structure, structure)| { - if let Some(st) = *close_structure { - let st_sample = Self::sample_column(column_gen, column_cache, st.pos, index); - if let Some(st_sample) = st_sample { - let st_sample = st_sample.clone(); - let st_info = match st.meta { - None => natural::structure_gen( - column_gen, - column_cache, - st.pos, - st.seed, - &st_sample, - index, - ), - Some(meta) => Some(StructureInfo { - pos: Vec3::from(st.pos) + Vec3::unit_z() * st_sample.alt as i32, - seed: st.seed, - meta, - }), - }; - if let Some(st_info) = st_info { - *structure = Some((st_info, st_sample)); - } - } - } - }); - - Some(ZCache { - wpos, - sample, - structures, - }) + Some(ZCache { sample }) } - pub fn get_with_z_cache( - &mut self, - wpos: Vec3, - z_cache: Option<&ZCache>, - only_structures: bool, - index: IndexRef<'a>, - ) -> Option { - let BlockGen { - column_cache, - column_gen, - } = self; + pub fn get_with_z_cache(&mut self, wpos: Vec3, z_cache: Option<&ZCache>) -> Option { + let BlockGen { column_gen } = self; let world = column_gen.sim; let z_cache = z_cache?; @@ -180,300 +66,131 @@ impl<'a> BlockGen<'a> { // marble, // marble_small, rock, - //cliffs, - cliff_hill, - close_cliffs, // temp, // humidity, stone_col, .. } = sample; - let structures = &z_cache.structures; - let wposf = wpos.map(|e| e as f64); - let (block, _height) = if !only_structures { - let (_definitely_underground, height, _on_cliff, basement_height, water_height) = - if (wposf.z as f32) < alt - 64.0 * chaos { - // Shortcut warping - (true, alt, false, basement, water_level) - } else { - // Apply warping - let warp = world - .gen_ctx - .warp_nz - .get(wposf.div(24.0)) - .mul((chaos - 0.1).max(0.0).min(1.0).powf(2.0)) - .mul(16.0); - let warp = Lerp::lerp(0.0, warp, warp_factor); + let (_definitely_underground, height, basement_height, water_height) = + if (wposf.z as f32) < alt - 64.0 * chaos { + // Shortcut warping + (true, alt, basement, water_level) + } else { + // Apply warping + let warp = world + .gen_ctx + .warp_nz + .get(wposf.div(24.0)) + .mul((chaos - 0.1).max(0.0).min(1.0).powf(2.0)) + .mul(16.0); + let warp = Lerp::lerp(0.0, warp, warp_factor); - let surface_height = alt + warp; + let height = alt + warp; - let (height, on_cliff) = if (wposf.z as f32) < alt + warp - 10.0 { - // Shortcut cliffs - (surface_height, false) + ( + false, + height, + basement + height - alt, + (if water_level <= alt { + water_level + warp } else { - let turb = Vec2::new( - world.gen_ctx.fast_turb_x_nz.get(wposf.div(25.0)) as f32, - world.gen_ctx.fast_turb_y_nz.get(wposf.div(25.0)) as f32, - ) * 8.0; - - let wpos_turb = Vec2::from(wpos).map(|e: i32| e as f32) + turb; - let cliff_height = Self::get_cliff_height( - column_gen, - column_cache, - wpos_turb, - &close_cliffs, - cliff_hill, - 0.0, - index, - ); - - ( - surface_height.max(cliff_height), - cliff_height > surface_height + 16.0, - ) - }; - - ( - false, - height, - on_cliff, - basement + height - alt, - (if water_level <= alt { - water_level + warp - } else { - water_level - }), - ) - }; - - // Sample blocks - - let water = Block::new(BlockKind::Water, Rgb::zero()); - - let grass_depth = (1.5 + 2.0 * chaos).min(height - basement_height); - let block = if (wposf.z as f32) < height - grass_depth { - let stone_factor = (height - grass_depth - wposf.z as f32) * 0.15; - let col = Lerp::lerp( - sub_surface_color, - stone_col.map(|e| e as f32 / 255.0), - stone_factor, + water_level + }), ) - .map(|e| (e * 255.0) as u8); + }; - if stone_factor >= 0.5 { - Some(Block::new(BlockKind::Rock, col)) + // Sample blocks + + let water = Block::new(BlockKind::Water, Rgb::zero()); + + let grass_depth = (1.5 + 2.0 * chaos).min(height - basement_height); + if (wposf.z as f32) < height - grass_depth { + let stone_factor = (height - grass_depth - wposf.z as f32) * 0.15; + let col = Lerp::lerp( + sub_surface_color, + stone_col.map(|e| e as f32 / 255.0), + stone_factor, + ) + .map(|e| (e * 255.0) as u8); + + if stone_factor >= 0.5 { + Some(Block::new(BlockKind::Rock, col)) + } else { + Some(Block::new(BlockKind::Earth, col)) + } + } else if (wposf.z as f32) < height { + let grass_factor = (wposf.z as f32 - (height - grass_depth)) + .div(grass_depth) + .powf(0.5); + let col = Lerp::lerp(sub_surface_color, surface_color, grass_factor); + // Surface + Some(Block::new( + if grass_factor > 0.7 { + BlockKind::Grass } else { - Some(Block::new(BlockKind::Earth, col)) - } - } else if (wposf.z as f32) < height { - let grass_factor = (wposf.z as f32 - (height - grass_depth)) - .div(grass_depth) - .powf(0.5); - let col = Lerp::lerp(sub_surface_color, surface_color, grass_factor); - // Surface + BlockKind::Earth + }, + col.map(|e| (e * 255.0) as u8), + )) + } else { + None + } + .or_else(|| { + // Rocks + if (height + 2.5 - wposf.z as f32).div(7.5).abs().powf(2.0) < rock { + #[allow(clippy::identity_op)] + let field0 = RandomField::new(world.seed + 0); + let field1 = RandomField::new(world.seed + 1); + let field2 = RandomField::new(world.seed + 2); + Some(Block::new( - if grass_factor > 0.7 { - BlockKind::Grass - } else { - BlockKind::Earth - }, - col.map(|e| (e * 255.0) as u8), + BlockKind::WeakRock, + stone_col.map2( + Rgb::new( + field0.get(wpos) as u8 % 16, + field1.get(wpos) as u8 % 16, + field2.get(wpos) as u8 % 16, + ), + |stone, x| stone.saturating_sub(x), + ), )) } else { None } - .or_else(|| { - // Rocks - if (height + 2.5 - wposf.z as f32).div(7.5).abs().powf(2.0) < rock { - #[allow(clippy::identity_op)] - let field0 = RandomField::new(world.seed + 0); - let field1 = RandomField::new(world.seed + 1); - let field2 = RandomField::new(world.seed + 2); - - Some(Block::new( - BlockKind::WeakRock, - stone_col.map2( - Rgb::new( - field0.get(wpos) as u8 % 16, - field1.get(wpos) as u8 % 16, - field2.get(wpos) as u8 % 16, - ), - |stone, x| stone.saturating_sub(x), - ), - )) - } else { - None - } - }) - .or_else(|| { - // Water - if (wposf.z as f32) < water_height { - // Ocean - Some(water) - } else { - None - } - }); - - (block, height) - } else { - (None, sample.alt) - }; - - let block = structures - .iter() - .find_map(|st| { - let (st, st_sample) = st.as_ref()?; - st.get(index, wpos, st_sample) - }) - .or(block); - - block + }) + .or_else(|| { + // Water + if (wposf.z as f32) < water_height { + // Ocean + Some(water) + } else { + None + } + }) } } pub struct ZCache<'a> { - wpos: Vec2, pub sample: ColumnSample<'a>, - structures: [Option<(StructureInfo, ColumnSample<'a>)>; 9], } impl<'a> ZCache<'a> { - pub fn get_z_limits<'b>( - &self, - block_gen: &mut BlockGen<'b>, - index: IndexRef<'b>, - ) -> (f32, f32, f32) { + pub fn get_z_limits(&self) -> (f32, f32) { let min = self.sample.alt - (self.sample.chaos.min(1.0) * 16.0); let min = min - 4.0; - let cliff = BlockGen::get_cliff_height( - &block_gen.column_gen, - &mut block_gen.column_cache, - self.wpos.map(|e| e as f32), - &self.sample.close_cliffs, - self.sample.cliff_hill, - 32.0, - index, - ); - let rocks = if self.sample.rock > 0.0 { 12.0 } else { 0.0 }; let warp = self.sample.chaos * 32.0; - let (structure_min, structure_max) = self - .structures - .iter() - .filter_map(|st| st.as_ref()) - .fold((0.0f32, 0.0f32), |(min, max), (st_info, _st_sample)| { - let bounds = st_info.get_bounds(); - let st_area = Aabr { - min: Vec2::from(bounds.min), - max: Vec2::from(bounds.max), - }; + let ground_max = self.sample.alt + warp + rocks + 2.0; - if st_area.contains_point(self.wpos - st_info.pos) { - (min.min(bounds.min.z as f32), max.max(bounds.max.z as f32)) - } else { - (min, max) - } - }); + let max = ground_max.max(self.sample.water_level + 2.0); - let ground_max = (self.sample.alt + warp + rocks).max(cliff) + 2.0; - - let min = min + structure_min; - let max = (ground_max + structure_max).max(self.sample.water_level + 2.0); - - let structures_only_min_z = ground_max.max(self.sample.water_level + 2.0); - - (min, structures_only_min_z, max) - } -} - -#[derive(Copy, Clone)] -pub enum StructureMeta { - Pyramid { - height: i32, - }, - Volume { - units: (Vec2, Vec2), - volume: &'static Structure, - }, -} - -pub struct StructureInfo { - pos: Vec3, - seed: u32, - meta: StructureMeta, -} - -impl StructureInfo { - fn get_bounds(&self) -> Aabb { - match self.meta { - StructureMeta::Pyramid { height } => { - let base = 40; - Aabb { - min: Vec3::new(-base - height, -base - height, -base), - max: Vec3::new(base + height, base + height, height), - } - }, - StructureMeta::Volume { units, volume } => { - let bounds = volume.get_bounds(); - - (Aabb { - min: Vec3::from(units.0 * bounds.min.x + units.1 * bounds.min.y) - + Vec3::unit_z() * bounds.min.z, - max: Vec3::from(units.0 * bounds.max.x + units.1 * bounds.max.y) - + Vec3::unit_z() * bounds.max.z, - }) - .made_valid() - }, - } - } - - fn get(&self, index: IndexRef, wpos: Vec3, sample: &ColumnSample) -> Option { - match self.meta { - StructureMeta::Pyramid { height } => { - if wpos.z - self.pos.z - < height - - Vec2::from(wpos - self.pos) - .map(|e: i32| (e.abs() / 2) * 2) - .reduce_max() - { - Some(Block::new( - BlockKind::Rock, - index.colors.block.pyramid.into(), - )) - } else { - None - } - }, - StructureMeta::Volume { units, volume } => { - let rpos = wpos - self.pos; - let block_pos = Vec3::unit_z() * rpos.z - + Vec3::from(units.0) * rpos.x - + Vec3::from(units.1) * rpos.y; - - volume - .get((block_pos * 128) / 128) // Scaling - .ok() - .and_then(|b| { - block_from_structure( - index, - *b, - block_pos, - self.pos.into(), - self.seed, - sample, - // TODO: Take environment into account. - Block::air, - ) - }) - }, - } + (min, max) } } diff --git a/world/src/block/natural.rs b/world/src/block/natural.rs deleted file mode 100644 index 8ccff506b0..0000000000 --- a/world/src/block/natural.rs +++ /dev/null @@ -1,91 +0,0 @@ -use super::{BlockGen, StructureInfo, StructureMeta}; -use crate::{ - all::ForestKind, - column::{ColumnGen, ColumnSample}, - util::{RandomPerm, Sampler, SmallCache, UnitChooser}, - IndexRef, CONFIG, -}; -use common::terrain::Structure; -use lazy_static::lazy_static; -use std::{sync::Arc, u32}; -use vek::*; - -static VOLUME_RAND: RandomPerm = RandomPerm::new(0xDB21C052); -static UNIT_CHOOSER: UnitChooser = UnitChooser::new(0x700F4EC7); -static QUIRKY_RAND: RandomPerm = RandomPerm::new(0xA634460F); - -pub fn structure_gen<'a>( - column_gen: &ColumnGen<'a>, - column_cache: &mut SmallCache>>, - st_pos: Vec2, - st_seed: u32, - st_sample: &ColumnSample, - index: IndexRef<'a>, -) -> Option { - // Assuming it's a tree... figure out when it SHOULDN'T spawn - let random_seed = (st_seed as f64) / (u32::MAX as f64); - if (st_sample.tree_density as f64) < random_seed - || st_sample.alt < st_sample.water_level - || st_sample.spawn_rate < 0.5 - || st_sample.water_dist.map(|d| d < 8.0).unwrap_or(false) - || st_sample.path.map(|(d, _, _, _)| d < 12.0).unwrap_or(false) - { - return None; - } - - let cliff_height = BlockGen::get_cliff_height( - column_gen, - column_cache, - st_pos.map(|e| e as f32), - &st_sample.close_cliffs, - st_sample.cliff_hill, - 0.0, - index, - ); - - let wheight = st_sample.alt.max(cliff_height); - let st_pos3d = Vec3::new(st_pos.x, st_pos.y, wheight as i32); - - let volumes: &'static [_] = if QUIRKY_RAND.get(st_seed) % 512 == 17 { - if st_sample.temp > CONFIG.desert_temp { - &QUIRKY_DRY - } else { - &QUIRKY - } - } else { - match st_sample.forest_kind { - ForestKind::Palm => &PALMS, - ForestKind::Savannah => &ACACIAS, - ForestKind::Oak if QUIRKY_RAND.get(st_seed) % 16 == 7 => &OAK_STUMPS, - ForestKind::Oak if QUIRKY_RAND.get(st_seed) % 19 == 7 => &FRUIT_TREES, - ForestKind::Oak if QUIRKY_RAND.get(st_seed) % 14 == 7 => &BIRCHES, - ForestKind::Oak => &OAKS, - ForestKind::Pine => &PINES, - ForestKind::SnowPine => &SNOW_PINES, - ForestKind::Mangrove => &MANGROVE_TREES, - } - }; - - Some(StructureInfo { - pos: st_pos3d, - seed: st_seed, - meta: StructureMeta::Volume { - units: UNIT_CHOOSER.get(st_seed), - volume: &volumes[(VOLUME_RAND.get(st_seed) / 13) as usize % volumes.len()], - }, - }) -} - -lazy_static! { - pub static ref OAKS: Vec> = Structure::load_group("oaks"); - pub static ref OAK_STUMPS: Vec> = Structure::load_group("oak_stumps"); - pub static ref PINES: Vec> = Structure::load_group("pines"); - pub static ref PALMS: Vec> = Structure::load_group("palms"); - pub static ref SNOW_PINES: Vec> = Structure::load_group("snow_pines"); - pub static ref ACACIAS: Vec> = Structure::load_group("acacias"); - pub static ref FRUIT_TREES: Vec> = Structure::load_group("fruit_trees"); - pub static ref BIRCHES: Vec> = Structure::load_group("birch"); - pub static ref MANGROVE_TREES: Vec> = Structure::load_group("mangrove_trees"); - pub static ref QUIRKY: Vec> = Structure::load_group("quirky"); - pub static ref QUIRKY_DRY: Vec> = Structure::load_group("quirky_dry"); -} diff --git a/world/src/canvas.rs b/world/src/canvas.rs new file mode 100644 index 0000000000..d6ed1acd14 --- /dev/null +++ b/world/src/canvas.rs @@ -0,0 +1,96 @@ +use crate::{ + block::ZCache, + column::ColumnSample, + index::IndexRef, + sim::{SimChunk, WorldSim as Land}, + util::Grid, +}; +use common::{ + terrain::{Block, TerrainChunk, TerrainChunkSize}, + vol::{ReadVol, RectVolSize, WriteVol}, +}; +use std::ops::Deref; +use vek::*; + +#[derive(Copy, Clone)] +pub struct CanvasInfo<'a> { + pub(crate) wpos: Vec2, + pub(crate) column_grid: &'a Grid>>, + pub(crate) column_grid_border: i32, + pub(crate) land: &'a Land, + pub(crate) index: IndexRef<'a>, + pub(crate) chunk: &'a SimChunk, +} + +impl<'a> CanvasInfo<'a> { + pub fn wpos(&self) -> Vec2 { self.wpos } + + pub fn area(&self) -> Aabr { + Rect::from(( + self.wpos(), + Extent2::from(TerrainChunkSize::RECT_SIZE.map(|e| e as i32)), + )) + .into() + } + + pub fn col(&self, pos: Vec2) -> Option<&'a ColumnSample> { + self.column_grid + .get(self.column_grid_border + pos - self.wpos()) + .map(Option::as_ref) + .flatten() + .map(|zc| &zc.sample) + } + + pub fn index(&self) -> IndexRef<'a> { self.index } + + pub fn chunk(&self) -> &'a SimChunk { self.chunk } + + pub fn land(&self) -> &'a Land { self.land } +} + +pub struct Canvas<'a> { + pub(crate) info: CanvasInfo<'a>, + pub(crate) chunk: &'a mut TerrainChunk, +} + +impl<'a> Canvas<'a> { + /// The borrow checker complains at immutable features of canvas (column + /// sampling, etc.) being used at the same time as mutable features + /// (writing blocks). To avoid this, this method extracts the + /// inner `CanvasInfo` such that it may be used independently. + pub fn info(&mut self) -> CanvasInfo<'a> { self.info } + + pub fn get(&mut self, pos: Vec3) -> Option { + self.chunk.get(pos - self.wpos()).ok().copied() + } + + pub fn set(&mut self, pos: Vec3, block: Block) { + let _ = self.chunk.set(pos - self.wpos(), block); + } + + pub fn map(&mut self, pos: Vec3, f: impl FnOnce(Block) -> Block) { + let _ = self.chunk.map(pos - self.wpos(), f); + } + + /// Execute an operation upon each column in this canvas. + pub fn foreach_col(&mut self, mut f: impl FnMut(&mut Self, Vec2, &ColumnSample)) { + for y in 0..self.area().size().h as i32 { + for x in 0..self.area().size().w as i32 { + let wpos2d = self.wpos() + Vec2::new(x, y); + let info = self.info; + let col = if let Some(col) = info.col(wpos2d) { + col + } else { + return; + }; + f(self, wpos2d, col); + } + } + } +} + +impl<'a> Deref for Canvas<'a> { + type Target = CanvasInfo<'a>; + + fn deref(&self) -> &Self::Target { &self.info } +} diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index d49f610d4f..af33cfef87 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -646,16 +646,16 @@ fn walk_in_dir(sim: &WorldSim, a: Vec2, dir: Vec2) -> Option { let a_chunk = sim.get(a)?; let b_chunk = sim.get(a + dir)?; - let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 2.5).powf(2.0); + let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 5.0).powf(2.0); let water_cost = if b_chunk.river.near_water() { 50.0 } else { 0.0 - }; + } + (b_chunk.water_alt - b_chunk.alt + 8.0).clamped(0.0, 8.0) * 3.0; // Try not to path swamps / tidal areas let wild_cost = if b_chunk.path.0.is_way() { 0.0 // Traversing existing paths has no additional cost! } else { - 2.0 + 3.0 // + (1.0 - b_chunk.tree_density) * 20.0 // Prefer going through forests, for aesthetics }; Some(1.0 + hill_cost + water_cost + wild_cost) } else { diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 665b98fe86..466987a82a 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -1,6 +1,5 @@ use crate::{ all::ForestKind, - block::StructureMeta, sim::{local_cells, Cave, Path, RiverKind, SimChunk, WorldSim}, util::Sampler, IndexRef, CONFIG, @@ -54,56 +53,6 @@ pub struct Colors { impl<'a> ColumnGen<'a> { pub fn new(sim: &'a WorldSim) -> Self { Self { sim } } - - #[allow(clippy::if_same_then_else)] // TODO: Pending review in #587 - fn get_local_structure(&self, wpos: Vec2) -> Option { - let (pos, seed) = self - .sim - .gen_ctx - .region_gen - .get(wpos) - .iter() - .copied() - .min_by_key(|(pos, _)| pos.distance_squared(wpos)) - .unwrap(); - - let chunk_pos = pos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); - let chunk = self.sim.get(chunk_pos)?; - - if seed % 5 == 2 - && chunk.temp > CONFIG.desert_temp - && chunk.alt > chunk.water_alt + 5.0 - && chunk.chaos <= 0.35 - { - /*Some(StructureData { - pos, - seed, - meta: Some(StructureMeta::Pyramid { height: 140 }), - })*/ - None - } else { - None - } - } - - fn gen_close_structures(&self, wpos: Vec2) -> [Option; 9] { - let mut metas = [None; 9]; - self.sim - .gen_ctx - .structure_gen - .get(wpos) - .iter() - .copied() - .enumerate() - .for_each(|(i, (pos, seed))| { - metas[i] = self.get_local_structure(pos).or(Some(StructureData { - pos, - seed, - meta: None, - })); - }); - metas - } } impl<'a> Sampler<'a> for ColumnGen<'a> { @@ -456,12 +405,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { river_overlap_distance_product / overlap_count } as f32; - let cliff_hill = (sim - .gen_ctx - .small_nz - .get((wposf_turb.div(128.0)).into_array()) as f32) - .mul(4.0); - let riverless_alt_delta = (sim.gen_ctx.small_nz.get( (wposf_turb.div(200.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64))).into_array(), ) as f32) @@ -491,9 +434,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { }) .unwrap_or(CONFIG.sea_level); - let is_cliffs = sim_chunk.is_cliffs; - let near_cliffs = sim_chunk.near_cliffs; - let river_gouge = 0.5; let (_in_water, water_dist, alt_, water_level, riverless_alt, warp_factor) = if let Some( (max_border_river_pos, river_chunk, max_border_river, max_border_river_dist), @@ -508,43 +448,44 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { max_border_river .river_kind .and_then(|river_kind| { - if let RiverKind::River { cross_section } = river_kind { - if max_border_river_dist.map(|(_, dist, _, _)| dist) - != Some(Vec2::zero()) - { - return None; - } - let ( - _, - _, - river_width, - (river_t, (river_pos, _), downhill_river_chunk), - ) = max_border_river_dist.unwrap(); - let river_alt = Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river_chunk.alt.max(downhill_river_chunk.water_alt), - river_t as f32, - ); - let new_alt = river_alt - river_gouge; - let river_dist = wposf.distance(river_pos); - let river_height_factor = river_dist / (river_width * 0.5); + match river_kind { + RiverKind::River { cross_section } => { + if max_border_river_dist.map(|(_, dist, _, _)| dist) + != Some(Vec2::zero()) + { + return None; + } + let ( + _, + _, + river_width, + (river_t, (river_pos, _), downhill_river_chunk), + ) = max_border_river_dist.unwrap(); + let river_alt = Lerp::lerp( + river_chunk.alt.max(river_chunk.water_alt), + downhill_river_chunk.alt.max(downhill_river_chunk.water_alt), + river_t as f32, + ); + let new_alt = river_alt - river_gouge; + let river_dist = wposf.distance(river_pos); + let river_height_factor = river_dist / (river_width * 0.5); - let valley_alt = Lerp::lerp( - new_alt - cross_section.y.max(1.0), - new_alt - 1.0, - (river_height_factor * river_height_factor) as f32, - ); + let valley_alt = Lerp::lerp( + new_alt - cross_section.y.max(1.0), + new_alt - 1.0, + (river_height_factor * river_height_factor) as f32, + ); - Some(( - true, - Some((river_dist - river_width * 0.5) as f32), - valley_alt, - new_alt, - river_alt, - 0.0, - )) - } else { - None + Some(( + true, + Some((river_dist - river_width * 0.5) as f32), + valley_alt, + new_alt, + alt, //river_alt + cross_section.y.max(1.0), + 0.0, + )) + }, + _ => None, } }) .unwrap_or_else(|| { @@ -693,7 +634,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { } else { return ( true, - None, + Some(lake_dist as f32), alt_for_river, if in_bounds_ { downhill_water_alt.max(lake_water_alt) @@ -726,7 +667,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { Some((river_dist - river_width * 0.5) as f32), alt_for_river, downhill_water_alt, - alt_for_river, + alt, //alt_for_river, river_scale_factor as f32, ) }, @@ -737,7 +678,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { None, alt_for_river, downhill_water_alt, - alt_for_river, + alt, //alt_for_river, river_scale_factor as f32, )) }); @@ -755,7 +696,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { None, alt_for_river, downhill_water_alt, - alt_for_river, + alt, //alt_for_river, 1.0, ) }; @@ -780,6 +721,26 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { .max(0.0) .mul(8.0); + // Columns near water have a more stable temperature and so get pushed towards + // the average (0) + let temp = Lerp::lerp( + Lerp::lerp(temp, 0.0, 0.1), + temp, + water_dist + .map(|water_dist| water_dist / 20.0) + .unwrap_or(1.0) + .clamped(0.0, 1.0), + ); + // Columns near water get a humidity boost + let humidity = Lerp::lerp( + Lerp::lerp(humidity, 1.0, 0.25), + humidity, + water_dist + .map(|water_dist| water_dist / 20.0) + .unwrap_or(1.0) + .clamped(0.0, 1.0), + ); + let wposf3d = Vec3::new(wposf.x, wposf.y, alt as f64); let marble_small = (sim.gen_ctx.hill_nz.get((wposf3d.div(3.0)).into_array()) as f32) @@ -918,7 +879,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { humidity .sub(CONFIG.desert_hum) .div(CONFIG.forest_hum.sub(CONFIG.desert_hum)) - .mul(1.0), + .mul(1.25), ); // From forest to jungle humidity, we go from snow to dark grass to grass to // tropics to sand depending on temperature. @@ -986,15 +947,17 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { .max(-humidity.sub(CONFIG.desert_hum)) .mul(16.0) .add((marble_small - 0.5) * 0.5); - let (alt, ground, sub_surface_color) = if snow_cover <= 0.5 && alt > water_level { + let (alt, ground, sub_surface_color, snow_cover) = if snow_cover <= 0.5 && alt > water_level + { // Allow snow cover. ( alt + 1.0 - snow_cover.max(0.0), Rgb::lerp(snow, ground, snow_cover), Lerp::lerp(sub_surface_color, ground, alt.sub(basement).mul(0.15)), + true, ) } else { - (alt, ground, sub_surface_color) + (alt, ground, sub_surface_color, false) }; // Make river banks not have grass @@ -1004,7 +967,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { let near_ocean = max_river.and_then(|(_, _, river_data, _)| { if (river_data.is_lake() || river_data.river_kind == Some(RiverKind::Ocean)) - && ((alt <= water_level.max(CONFIG.sea_level + 5.0) && !is_cliffs) || !near_cliffs) + && alt <= water_level.max(CONFIG.sea_level + 5.0) { Some(water_level) } else { @@ -1053,14 +1016,9 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { 0.0 }, forest_kind: sim_chunk.forest_kind, - close_structures: self.gen_close_structures(wpos), marble, marble_small, rock, - is_cliffs, - near_cliffs, - cliff_hill, - close_cliffs: sim.gen_ctx.cliff_gen.get(wpos), temp, humidity, spawn_rate, @@ -1068,6 +1026,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { water_dist, path, cave, + snow_cover, chunk: sim_chunk, }) @@ -1086,14 +1045,9 @@ pub struct ColumnSample<'a> { pub sub_surface_color: Rgb, pub tree_density: f32, pub forest_kind: ForestKind, - pub close_structures: [Option; 9], pub marble: f32, pub marble_small: f32, pub rock: f32, - pub is_cliffs: bool, - pub near_cliffs: bool, - pub cliff_hill: f32, - pub close_cliffs: [(Vec2, u32); 9], pub temp: f32, pub humidity: f32, pub spawn_rate: f32, @@ -1101,13 +1055,7 @@ pub struct ColumnSample<'a> { pub water_dist: Option, pub path: Option<(f32, Vec2, Path, Vec2)>, pub cave: Option<(f32, Vec2, Cave, Vec2)>, + pub snow_cover: bool, pub chunk: &'a SimChunk, } - -#[derive(Copy, Clone)] -pub struct StructureData { - pub pos: Vec2, - pub seed: u32, - pub meta: Option, -} diff --git a/world/src/config.rs b/world/src/config.rs index 5f8914d6e1..b3e841fce7 100644 --- a/world/src/config.rs +++ b/world/src/config.rs @@ -59,7 +59,7 @@ pub const CONFIG: Config = Config { desert_temp: 0.8, desert_hum: 0.15, forest_hum: 0.5, - jungle_hum: 0.85, + jungle_hum: 0.75, rainfall_chunk_rate: 1.0 / (512.0 * 32.0 * 32.0), river_roughness: 0.06125, river_max_width: 2.0, diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 666664f49c..77b9b1e1e0 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -1,11 +1,12 @@ pub mod scatter; +pub mod tree; -pub use self::scatter::apply_scatter_to; +pub use self::{scatter::apply_scatter_to, tree::apply_trees_to}; use crate::{ column::ColumnSample, util::{RandomField, Sampler}, - IndexRef, + Canvas, IndexRef, }; use common::{ assets::Asset, @@ -32,180 +33,162 @@ pub struct Colors { const EMPTY_AIR: Block = Block::air(SpriteKind::Empty); -pub fn apply_paths_to<'a>( - wpos2d: Vec2, - mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, - vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), - index: IndexRef, -) { - for y in 0..vol.size_xy().y as i32 { - for x in 0..vol.size_xy().x as i32 { - let offs = Vec2::new(x, y); +pub fn apply_paths_to(canvas: &mut Canvas) { + let info = canvas.info(); + canvas.foreach_col(|canvas, wpos2d, col| { + let surface_z = col.riverless_alt.floor() as i32; - let wpos2d = wpos2d + offs; + let noisy_color = |color: Rgb, factor: u32| { + let nz = RandomField::new(0).get(Vec3::new(wpos2d.x, wpos2d.y, surface_z)); + color.map(|e| { + (e as u32 + nz % (factor * 2)) + .saturating_sub(factor) + .min(255) as u8 + }) + }; - // Sample terrain - let col_sample = if let Some(col_sample) = get_column(offs) { - col_sample - } else { - continue; + if let Some((path_dist, path_nearest, path, _)) = + col.path.filter(|(dist, _, path, _)| *dist < path.width) + { + let inset = 0; + + // Try to use the column at the centre of the path for sampling to make them + // flatter + let col_pos = -info.wpos().map(|e| e as f32) + path_nearest; + let col00 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0)); + let col10 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0)); + let col01 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1)); + let col11 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1)); + let col_attr = |col: &ColumnSample| { + Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0)) }; - let surface_z = col_sample.riverless_alt.floor() as i32; + let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) { + (Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp( + Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()), + Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()), + path_nearest.y.fract(), + ), + _ => col_attr(col), + } + .into_array(); + let (bridge_offset, depth) = ( + ((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0, + ((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs()) + * (riverless_alt + 5.0 - alt).max(0.0) + * 1.75 + + 3.0) as i32, + ); + let surface_z = (riverless_alt + bridge_offset).floor() as i32; - let noisy_color = |col: Rgb, factor: u32| { - let nz = RandomField::new(0).get(Vec3::new(wpos2d.x, wpos2d.y, surface_z)); - col.map(|e| { - (e as u32 + nz % (factor * 2)) - .saturating_sub(factor) - .min(255) as u8 - }) - }; - - if let Some((path_dist, path_nearest, path, _)) = col_sample - .path - .filter(|(dist, _, path, _)| *dist < path.width) - { - let inset = 0; - - // Try to use the column at the centre of the path for sampling to make them - // flatter - let col_pos = (offs - wpos2d).map(|e| e as f32) + path_nearest; - let col00 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0)); - let col10 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0)); - let col01 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1)); - let col11 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1)); - let col_attr = |col: &ColumnSample| { - Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0)) - }; - let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) { - (Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp( - Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()), - Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()), - path_nearest.y.fract(), - ), - _ => col_attr(col_sample), - } - .into_array(); - let (bridge_offset, depth) = ( - ((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0, - ((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs()) - * (riverless_alt + 5.0 - alt).max(0.0) - * 1.75 - + 3.0) as i32, + for z in inset - depth..inset { + let _ = canvas.set( + Vec3::new(wpos2d.x, wpos2d.y, surface_z + z), + if bridge_offset >= 2.0 && path_dist >= 3.0 || z < inset - 1 { + Block::new( + BlockKind::Rock, + noisy_color(info.index().colors.layer.bridge.into(), 8), + ) + } else { + let path_color = + path.surface_color(col.sub_surface_color.map(|e| (e * 255.0) as u8)); + Block::new(BlockKind::Earth, noisy_color(path_color, 8)) + }, ); - let surface_z = (riverless_alt + bridge_offset).floor() as i32; - - for z in inset - depth..inset { - let _ = vol.set( - Vec3::new(offs.x, offs.y, surface_z + z), - if bridge_offset >= 2.0 && path_dist >= 3.0 || z < inset - 1 { - Block::new( - BlockKind::Rock, - noisy_color(index.colors.layer.bridge.into(), 8), - ) - } else { - let path_color = path.surface_color( - col_sample.sub_surface_color.map(|e| (e * 255.0) as u8), - ); - Block::new(BlockKind::Earth, noisy_color(path_color, 8)) - }, - ); - } - let head_space = path.head_space(path_dist); - for z in inset..inset + head_space { - let pos = Vec3::new(offs.x, offs.y, surface_z + z); - if vol.get(pos).unwrap().kind() != BlockKind::Water { - let _ = vol.set(pos, EMPTY_AIR); - } + } + let head_space = path.head_space(path_dist); + for z in inset..inset + head_space { + let pos = Vec3::new(wpos2d.x, wpos2d.y, surface_z + z); + if canvas.get(pos).unwrap().kind() != BlockKind::Water { + let _ = canvas.set(pos, EMPTY_AIR); } } } - } + }); } -pub fn apply_caves_to<'a>( - wpos2d: Vec2, - mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, - vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), - index: IndexRef, -) { - for y in 0..vol.size_xy().y as i32 { - for x in 0..vol.size_xy().x as i32 { - let offs = Vec2::new(x, y); +pub fn apply_caves_to(canvas: &mut Canvas) { + let info = canvas.info(); + canvas.foreach_col(|canvas, wpos2d, col| { + let surface_z = col.riverless_alt.floor() as i32; - let wpos2d = wpos2d + offs; + if let Some((cave_dist, _, cave, _)) = + col.cave.filter(|(dist, _, cave, _)| *dist < cave.width) + { + let cave_x = (cave_dist / cave.width).min(1.0); - // Sample terrain - let col_sample = if let Some(col_sample) = get_column(offs) { - col_sample - } else { - continue; - }; - let surface_z = col_sample.riverless_alt.floor() as i32; + // Relative units + let cave_floor = 0.0 - 0.5 * (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width; + let cave_height = (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width; - if let Some((cave_dist, _, cave, _)) = col_sample - .cave - .filter(|(dist, _, cave, _)| *dist < cave.width) - { - let cave_x = (cave_dist / cave.width).min(1.0); + // Abs units + let cave_base = (cave.alt + cave_floor) as i32; + let cave_roof = (cave.alt + cave_height) as i32; - // Relative units - let cave_floor = 0.0 - 0.5 * (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width; - let cave_height = (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width; - - // Abs units - let cave_base = (cave.alt + cave_floor) as i32; - let cave_roof = (cave.alt + cave_height) as i32; - - for z in cave_base..cave_roof { - if cave_x < 0.95 - || index.noise.cave_nz.get( - Vec3::new(wpos2d.x, wpos2d.y, z) - .map(|e| e as f64 * 0.15) - .into_array(), - ) < 0.0 - { - let _ = vol.set(Vec3::new(offs.x, offs.y, z), EMPTY_AIR); - } - } - - // Stalagtites - let stalagtites = index - .noise - .cave_nz - .get(wpos2d.map(|e| e as f64 * 0.125).into_array()) - .sub(0.5) - .max(0.0) - .mul( - (col_sample.alt - cave_roof as f32 - 5.0) - .mul(0.15) - .clamped(0.0, 1.0) as f64, - ) - .mul(45.0) as i32; - - for z in cave_roof - stalagtites..cave_roof { - let _ = vol.set( - Vec3::new(offs.x, offs.y, z), - Block::new(BlockKind::WeakRock, index.colors.layer.stalagtite.into()), - ); - } - - let cave_depth = (col_sample.alt - cave.alt).max(0.0); - let difficulty = cave_depth / 100.0; - - // Scatter things in caves - if RandomField::new(index.seed).chance(wpos2d.into(), 0.001 * difficulty.powf(1.5)) - && cave_base < surface_z as i32 - 25 + for z in cave_base..cave_roof { + if cave_x < 0.95 + || info.index().noise.cave_nz.get( + Vec3::new(wpos2d.x, wpos2d.y, z) + .map(|e| e as f64 * 0.15) + .into_array(), + ) < 0.0 { - let kind = *Lottery::::load_expect("common.cave_scatter") - .choose_seeded(RandomField::new(index.seed + 1).get(wpos2d.into())); - let _ = vol.map(Vec3::new(offs.x, offs.y, cave_base), |block| { - block.with_sprite(kind) + // If the block a little above is liquid, we should stop carving out the cave in + // order to leave a ceiling, and not floating water + if canvas + .get(Vec3::new(wpos2d.x, wpos2d.y, z + 2)) + .map(|b| b.is_liquid()) + .unwrap_or(false) + { + break; + } + + canvas.map(Vec3::new(wpos2d.x, wpos2d.y, z), |b| { + if b.is_liquid() { b } else { EMPTY_AIR } }); } } + + // Stalagtites + let stalagtites = info + .index() + .noise + .cave_nz + .get(wpos2d.map(|e| e as f64 * 0.125).into_array()) + .sub(0.5) + .max(0.0) + .mul( + (col.alt - cave_roof as f32 - 5.0) + .mul(0.15) + .clamped(0.0, 1.0) as f64, + ) + .mul(45.0) as i32; + + for z in cave_roof - stalagtites..cave_roof { + canvas.set( + Vec3::new(wpos2d.x, wpos2d.y, z), + Block::new( + BlockKind::WeakRock, + info.index().colors.layer.stalagtite.into(), + ), + ); + } + + let cave_depth = (col.alt - cave.alt).max(0.0); + let difficulty = cave_depth / 100.0; + + // Scatter things in caves + if RandomField::new(info.index().seed) + .chance(wpos2d.into(), 0.001 * difficulty.powf(1.5)) + && cave_base < surface_z as i32 - 25 + { + let kind = *Lottery::::load_expect("common.cave_scatter") + .choose_seeded(RandomField::new(info.index().seed + 1).get(wpos2d.into())); + canvas.map(Vec3::new(wpos2d.x, wpos2d.y, cave_base), |block| { + block.with_sprite(kind) + }); + } } - } + }); } #[allow(clippy::eval_order_dependence)] pub fn apply_caves_supplement<'a>( diff --git a/world/src/layer/scatter.rs b/world/src/layer/scatter.rs index 2a3746a114..d5e74015d3 100644 --- a/world/src/layer/scatter.rs +++ b/world/src/layer/scatter.rs @@ -1,8 +1,5 @@ -use crate::{column::ColumnSample, sim::SimChunk, util::RandomField, IndexRef, CONFIG}; -use common::{ - terrain::{Block, SpriteKind}, - vol::{BaseVol, ReadVol, RectSizedVol, WriteVol}, -}; +use crate::{column::ColumnSample, sim::SimChunk, util::RandomField, Canvas, CONFIG}; +use common::terrain::SpriteKind; use noise::NoiseFn; use std::f32; use vek::*; @@ -11,13 +8,7 @@ fn close(x: f32, tgt: f32, falloff: f32) -> f32 { (1.0 - (x - tgt).abs() / falloff).max(0.0).powf(0.125) } const MUSH_FACT: f32 = 1.0e-4; // To balance everything around the mushroom spawning rate -pub fn apply_scatter_to<'a>( - wpos2d: Vec2, - mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, - vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), - index: IndexRef, - chunk: &SimChunk, -) { +pub fn apply_scatter_to(canvas: &mut Canvas) { use SpriteKind::*; #[allow(clippy::type_complexity)] // TODO: Add back all sprites we had before @@ -286,76 +277,65 @@ pub fn apply_scatter_to<'a>( (Chest, true, |_, _| (MUSH_FACT * 0.1, None)), ]; - for y in 0..vol.size_xy().y as i32 { - for x in 0..vol.size_xy().x as i32 { - let offs = Vec2::new(x, y); + canvas.foreach_col(|canvas, wpos2d, col| { + let underwater = col.water_level > col.alt; - let wpos2d = wpos2d + offs; - - // Sample terrain - let col_sample = if let Some(col_sample) = get_column(offs) { - col_sample - } else { - continue; - }; - - let underwater = col_sample.water_level > col_sample.alt; - - let kind = scatter - .iter() - .enumerate() - .find_map(|(i, (kind, is_underwater, f))| { - let (density, patch) = f(chunk, col_sample); - let is_patch = patch - .map(|(wavelen, threshold)| { - index - .noise - .scatter_nz - .get( - wpos2d - .map(|e| e as f64 / wavelen as f64 + i as f64 * 43.0) - .into_array(), - ) - .abs() - > 1.0 - threshold as f64 - }) - .unwrap_or(true); - if density > 0.0 - && is_patch - && RandomField::new(i as u32) - .chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density) - && underwater == *is_underwater - { - Some(*kind) - } else { - None - } - }); - - if let Some(kind) = kind { - let alt = col_sample.alt as i32; - - // Find the intersection between ground and air, if there is one near the - // surface - if let Some(solid_end) = (-4..8) - .find(|z| { - vol.get(Vec3::new(offs.x, offs.y, alt + z)) - .map(|b| b.is_solid()) - .unwrap_or(false) - }) - .and_then(|solid_start| { - (1..8).map(|z| solid_start + z).find(|z| { - vol.get(Vec3::new(offs.x, offs.y, alt + z)) - .map(|b| !b.is_solid()) - .unwrap_or(true) - }) + let kind = scatter + .iter() + .enumerate() + .find_map(|(i, (kind, is_underwater, f))| { + let (density, patch) = f(canvas.chunk(), col); + let is_patch = patch + .map(|(wavelen, threshold)| { + canvas + .index() + .noise + .scatter_nz + .get( + wpos2d + .map(|e| e as f64 / wavelen as f64 + i as f64 * 43.0) + .into_array(), + ) + .abs() + > 1.0 - threshold as f64 }) + .unwrap_or(true); + if density > 0.0 + && is_patch + && RandomField::new(i as u32).chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density) + && underwater == *is_underwater { - let _ = vol.map(Vec3::new(offs.x, offs.y, alt + solid_end), |block| { - block.with_sprite(kind) - }); + Some(*kind) + } else { + None } + }); + + if let Some(kind) = kind { + let alt = col.alt as i32; + + // Find the intersection between ground and air, if there is one near the + // surface + if let Some(solid_end) = (-4..8) + .find(|z| { + canvas + .get(Vec3::new(wpos2d.x, wpos2d.y, alt + z)) + .map(|b| b.is_solid()) + .unwrap_or(false) + }) + .and_then(|solid_start| { + (1..8).map(|z| solid_start + z).find(|z| { + canvas + .get(Vec3::new(wpos2d.x, wpos2d.y, alt + z)) + .map(|b| !b.is_solid()) + .unwrap_or(true) + }) + }) + { + canvas.map(Vec3::new(wpos2d.x, wpos2d.y, alt + solid_end), |block| { + block.with_sprite(kind) + }); } } - } + }); } diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs new file mode 100644 index 0000000000..9228646ba2 --- /dev/null +++ b/world/src/layer/tree.rs @@ -0,0 +1,145 @@ +use crate::{ + all::ForestKind, + block::block_from_structure, + column::ColumnGen, + util::{RandomPerm, Sampler, UnitChooser}, + Canvas, CONFIG, +}; +use common::{ + terrain::{structure::Structure, Block, BlockKind}, + vol::ReadVol, +}; +use lazy_static::lazy_static; +use std::{collections::HashMap, f32, sync::Arc}; +use vek::*; + +lazy_static! { + pub static ref OAKS: Vec> = Structure::load_group("oaks"); + pub static ref OAK_STUMPS: Vec> = Structure::load_group("oak_stumps"); + pub static ref PINES: Vec> = Structure::load_group("pines"); + pub static ref PALMS: Vec> = Structure::load_group("palms"); + pub static ref ACACIAS: Vec> = Structure::load_group("acacias"); + pub static ref FRUIT_TREES: Vec> = Structure::load_group("fruit_trees"); + pub static ref BIRCHES: Vec> = Structure::load_group("birch"); + pub static ref MANGROVE_TREES: Vec> = Structure::load_group("mangrove_trees"); + pub static ref QUIRKY: Vec> = Structure::load_group("quirky"); + pub static ref QUIRKY_DRY: Vec> = Structure::load_group("quirky_dry"); +} + +static MODEL_RAND: RandomPerm = RandomPerm::new(0xDB21C052); +static UNIT_CHOOSER: UnitChooser = UnitChooser::new(0x700F4EC7); +static QUIRKY_RAND: RandomPerm = RandomPerm::new(0xA634460F); + +pub fn apply_trees_to(canvas: &mut Canvas) { + struct Tree { + pos: Vec3, + model: Arc, + seed: u32, + units: (Vec2, Vec2), + } + + let mut tree_cache = HashMap::new(); + + let info = canvas.info(); + canvas.foreach_col(|canvas, wpos2d, col| { + let trees = info.land().get_near_trees(wpos2d); + + for (tree_wpos, seed) in trees { + let tree = if let Some(tree) = tree_cache.entry(tree_wpos).or_insert_with(|| { + let col = ColumnGen::new(info.land()).get((tree_wpos, info.index()))?; + + // Ensure that it's valid to place a tree here + if ((seed.wrapping_mul(13)) & 0xFF) as f32 / 256.0 > col.tree_density + || col.alt < col.water_level + || col.spawn_rate < 0.5 + || col.water_dist.map(|d| d < 8.0).unwrap_or(false) + || col.path.map(|(d, _, _, _)| d < 12.0).unwrap_or(false) + { + return None; + } + + Some(Tree { + pos: Vec3::new(tree_wpos.x, tree_wpos.y, col.alt as i32), + model: { + let models: &'static [_] = if QUIRKY_RAND.get(seed) % 512 == 17 { + if col.temp > CONFIG.desert_temp { + &QUIRKY_DRY + } else { + &QUIRKY + } + } else { + match col.forest_kind { + ForestKind::Oak if QUIRKY_RAND.get(seed) % 16 == 7 => &OAK_STUMPS, + ForestKind::Oak if QUIRKY_RAND.get(seed) % 19 == 7 => &FRUIT_TREES, + ForestKind::Palm => &PALMS, + ForestKind::Savannah => &ACACIAS, + ForestKind::Oak => &OAKS, + ForestKind::Pine => &PINES, + ForestKind::Birch => &BIRCHES, + ForestKind::Mangrove => &MANGROVE_TREES, + } + }; + Arc::clone( + &models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize + % models.len()], + ) + }, + seed, + units: UNIT_CHOOSER.get(seed), + }) + }) { + tree + } else { + continue; + }; + + let bounds = tree.model.get_bounds(); + let mut is_top = true; + let mut is_leaf_top = true; + for z in (bounds.min.z..bounds.max.z).rev() { + let wpos = Vec3::new(wpos2d.x, wpos2d.y, tree.pos.z + z); + let model_pos = Vec3::from( + (wpos - tree.pos) + .xy() + .map2(Vec2::new(tree.units.0, tree.units.1), |rpos, unit| { + unit * rpos + }) + .sum(), + ) + Vec3::unit_z() * (wpos.z - tree.pos.z); + block_from_structure( + info.index(), + if let Some(block) = tree.model.get(model_pos).ok().copied() { + block + } else { + // If we hit an inaccessible block, we're probably outside the model bounds. + // Skip this column. + break; + }, + wpos, + tree.pos.xy(), + tree.seed, + col, + Block::air, + ) + .map(|block| { + // Add a snow covering to the block above under certain circumstances + if col.snow_cover + && ((block.kind() == BlockKind::Leaves && is_leaf_top) + || (is_top && block.is_filled())) + { + canvas.set( + wpos + Vec3::unit_z(), + Block::new(BlockKind::Snow, Rgb::new(210, 210, 255)), + ); + } + canvas.set(wpos, block); + is_leaf_top = false; + is_top = false; + }) + .unwrap_or_else(|| { + is_leaf_top = true; + }); + } + } + }); +} diff --git a/world/src/lib.rs b/world/src/lib.rs index 7aa02fb46c..9f6812dfed 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -8,11 +8,13 @@ const_generics, const_panic, label_break_value, - or_patterns + or_patterns, + array_value_iter )] mod all; mod block; +pub mod canvas; pub mod civ; mod column; pub mod config; @@ -25,7 +27,10 @@ pub mod site; pub mod util; // Reexports -pub use crate::config::CONFIG; +pub use crate::{ + canvas::{Canvas, CanvasInfo}, + config::CONFIG, +}; pub use block::BlockGen; pub use column::ColumnSample; pub use index::{IndexOwned, IndexRef}; @@ -126,7 +131,6 @@ impl World { ); let water = Block::new(BlockKind::Water, Rgb::zero()); - let _chunk_size2d = TerrainChunkSize::RECT_SIZE; let (base_z, sim_chunk) = match self .sim /*.get_interpolated( @@ -168,8 +172,7 @@ impl World { _ => continue, }; - let (min_z, only_structures_min_z, max_z) = - z_cache.get_z_limits(&mut sampler, index); + let (min_z, max_z) = z_cache.get_z_limits(); (base_z..min_z as i32).for_each(|z| { let _ = chunk.set(Vec3::new(x, y, z), stone); @@ -178,11 +181,8 @@ impl World { (min_z as i32..max_z as i32).for_each(|z| { let lpos = Vec3::new(x, y, z); let wpos = Vec3::from(chunk_wpos2d) + lpos; - let only_structures = lpos.z >= only_structures_min_z as i32; - if let Some(block) = - sampler.get_with_z_cache(wpos, Some(&z_cache), only_structures, index) - { + if let Some(block) = sampler.get_with_z_cache(wpos, Some(&z_cache)) { let _ = chunk.set(lpos, block); } }); @@ -201,9 +201,22 @@ impl World { let mut dynamic_rng = rand::thread_rng(); // Apply layers (paths, caves, etc.) - layer::apply_caves_to(chunk_wpos2d, sample_get, &mut chunk, index); - layer::apply_scatter_to(chunk_wpos2d, sample_get, &mut chunk, index, sim_chunk); - layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk, index); + let mut canvas = Canvas { + info: CanvasInfo { + wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + column_grid: &zcache_grid, + column_grid_border: grid_border, + land: &self.sim, + index, + chunk: sim_chunk, + }, + chunk: &mut chunk, + }; + + layer::apply_trees_to(&mut canvas); + layer::apply_scatter_to(&mut canvas); + layer::apply_caves_to(&mut canvas); + layer::apply_paths_to(&mut canvas); // Apply site generation sim_chunk.sites.iter().for_each(|site| { diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 5f67f0712b..e099bde409 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -28,7 +28,10 @@ use crate::{ civ::Place, column::ColumnGen, site::Site, - util::{seed_expan, FastNoise, RandomField, Sampler, StructureGen2d, LOCALITY, NEIGHBORS}, + util::{ + seed_expan, FastNoise, RandomField, RandomPerm, Sampler, StructureGen2d, LOCALITY, + NEIGHBORS, + }, IndexRef, CONFIG, }; use common::{ @@ -103,7 +106,6 @@ pub(crate) struct GenCtx { // Small amounts of noise for simulating rough terrain. pub small_nz: BasicMulti, pub rock_nz: HybridMulti, - pub cliff_nz: HybridMulti, pub warp_nz: FastNoise, pub tree_nz: BasicMulti, @@ -112,7 +114,6 @@ pub(crate) struct GenCtx { pub structure_gen: StructureGen2d, pub region_gen: StructureGen2d, - pub cliff_gen: StructureGen2d, pub fast_turb_x_nz: FastNoise, pub fast_turb_y_nz: FastNoise, @@ -503,7 +504,6 @@ impl WorldSim { small_nz: BasicMulti::new().set_octaves(2).set_seed(rng.gen()), rock_nz: HybridMulti::new().set_persistence(0.3).set_seed(rng.gen()), - cliff_nz: HybridMulti::new().set_persistence(0.3).set_seed(rng.gen()), warp_nz: FastNoise::new(rng.gen()), tree_nz: BasicMulti::new() .set_octaves(12) @@ -514,7 +514,6 @@ impl WorldSim { structure_gen: StructureGen2d::new(rng.gen(), 32, 16), region_gen: StructureGen2d::new(rng.gen(), 400, 96), - cliff_gen: StructureGen2d::new(rng.gen(), 80, 56), humid_nz: Billow::new() .set_octaves(9) .set_persistence(0.4) @@ -1431,25 +1430,11 @@ impl WorldSim { .into_par_iter() .map_init( || Box::new(BlockGen::new(ColumnGen::new(self))), - |block_gen, posi| { - let wpos = uniform_idx_as_vec2(self.map_size_lg(), posi); - let mut sample = column_sample.get( + |_block_gen, posi| { + let sample = column_sample.get( (uniform_idx_as_vec2(self.map_size_lg(), posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), index) )?; - let alt = sample.alt; - /* let z_cache = block_gen.get_z_cache(wpos); - sample.alt = alt.max(z_cache.get_z_limits(&mut block_gen).2); */ - sample.alt = alt.max(BlockGen::get_cliff_height( - &block_gen.column_gen, - &mut block_gen.column_cache, - wpos.map(|e| e as f32), - &sample.close_cliffs, - sample.cliff_hill, - 32.0, - index, - )); - sample.basement += sample.alt - alt; // sample.water_level = CONFIG.sea_level.max(sample.water_level); Some(sample) @@ -1997,6 +1982,14 @@ impl WorldSim { pub fn get_nearest_cave(&self, wpos: Vec2) -> Option<(f32, Vec2, Cave, Vec2)> { self.get_nearest_way(wpos, |chunk| Some(chunk.cave)) } + + /// Return an iterator over candidate tree positions (note that only some of + /// these will become trees since environmental parameters may forbid + /// them spawning). + pub fn get_near_trees(&self, wpos: Vec2) -> impl Iterator, u32)> + '_ { + // Deterministic based on wpos + std::array::IntoIter::new(self.gen_ctx.structure_gen.get(wpos)) + } } #[derive(Debug)] @@ -2010,8 +2003,6 @@ pub struct SimChunk { pub temp: f32, pub humidity: f32, pub rockiness: f32, - pub is_cliffs: bool, - pub near_cliffs: bool, pub tree_density: f32, pub forest_kind: ForestKind, pub spawn_rate: f32, @@ -2068,11 +2059,6 @@ impl SimChunk { // Even less granular--if this matters we can make the sign affect the quantity slightly. let abs_lat_uniform = latitude_uniform.abs(); */ - // Take the weighted average of our randomly generated base humidity, and the - // calculated water flux over this point in order to compute humidity. - const HUMID_WEIGHTS: [f32; 2] = [2.0, 1.0]; - let humidity = cdf_irwin_hall(&HUMID_WEIGHTS, [humid_uniform, flux_uniform]); - // We also correlate temperature negatively with altitude and absolute latitude, // using different weighting than we use for humidity. const TEMP_WEIGHTS: [f32; 2] = [/* 1.5, */ 1.0, 2.0]; @@ -2087,6 +2073,18 @@ impl SimChunk { .sub(0.5) .mul(2.0); + // Take the weighted average of our randomly generated base humidity, and the + // calculated water flux over this point in order to compute humidity. + const HUMID_WEIGHTS: [f32; 3] = [1.0, 1.0, 0.75]; + let humidity = cdf_irwin_hall(&HUMID_WEIGHTS, [humid_uniform, flux_uniform, 1.0]); + // Moisture evaporates more in hot places + let humidity = humidity + * (1.0 + - (temp - CONFIG.tropical_temp) + .max(0.0) + .div(1.0 - CONFIG.tropical_temp)) + .max(0.0); + let mut alt = CONFIG.sea_level.add(alt_pre); let basement = CONFIG.sea_level.add(basement_pre); let water_alt = CONFIG.sea_level.add(water_alt_pre); @@ -2101,10 +2099,6 @@ impl SimChunk { ) }; - //let cliff = gen_ctx.cliff_nz.get((wposf.div(2048.0)).into_array()) as f32 + - // chaos * 0.2; - let cliff = 0.0; // Disable cliffs - // Logistic regression. Make sure x ∈ (0, 1). let logit = |x: f64| x.ln() - x.neg().ln_1p(); // 0.5 + 0.5 * tanh(ln(1 / (1 - 0.1) - 1) / (2 * (sqrt(3)/pi))) @@ -2154,7 +2148,6 @@ impl SimChunk { .mul(1.5) .add(1.0) .mul(0.5) - .mul(1.2 - chaos as f64 * 0.95) .add(0.05) .max(0.0) .min(1.0); @@ -2165,13 +2158,31 @@ impl SimChunk { 1.0 } else { // Weighted logit sum. - logistic_cdf(logit(humidity as f64) + 0.5 * logit(tree_density)) + logistic_cdf(logit(tree_density)) } // rescale to (-0.95, 0.95) .sub(0.5) - .mul(0.95) .add(0.5) } as f32; + const MIN_TREE_HUM: f32 = 0.15; + // Tree density increases exponentially with humidity... + let tree_density = (tree_density * (humidity - MIN_TREE_HUM).max(0.0).mul(1.0 + MIN_TREE_HUM) / temp.max(0.75)) + // ...but is ultimately limited by available sunlight (and our tree generation system) + .min(1.0); + + // Sand dunes (formed over a short period of time) + let alt = alt + + if river.near_water() { + 0.0 + } else { + let warp = Vec2::new( + gen_ctx.turb_x_nz.get(wposf.div(256.0).into_array()) as f32, + gen_ctx.turb_y_nz.get(wposf.div(256.0).into_array()) as f32, + ) * 192.0; + let dune_nz = (wposf.map(|e| e as f32) + warp).sum().div(100.0).sin() * 0.5 + 0.5; + let dune_scale = 16.0; + dune_nz * dune_scale * (temp - 0.75).clamped(0.0, 0.25) * 4.0 + }; Self { chaos, @@ -2191,67 +2202,71 @@ impl SimChunk { } else { 0.0 }, - is_cliffs: cliff > 0.5 && !is_underwater, - near_cliffs: cliff > 0.2, tree_density, - forest_kind: if temp > CONFIG.temperate_temp { - if temp > CONFIG.desert_temp { - if humidity > CONFIG.jungle_hum { - // Forests in desert temperatures with extremely high humidity - // should probably be different from palm trees, but we use them - // for now. - ForestKind::Palm - } else if humidity > CONFIG.forest_hum { - ForestKind::Palm - } else if humidity > CONFIG.desert_hum { - // Low but not desert humidity, so we should really have some other - // terrain... - ForestKind::Savannah - } else { - ForestKind::Savannah - } - } else if temp > CONFIG.tropical_temp { - if humidity > CONFIG.jungle_hum { - if tree_density > 0.0 { - // println!("Mangrove: {:?}", wposf); - } - ForestKind::Mangrove - } else if humidity > CONFIG.forest_hum { - // NOTE: Probably the wrong kind of tree for this climate. - ForestKind::Oak - } else if humidity > CONFIG.desert_hum { - // Low but not desert... need something besides savannah. - ForestKind::Savannah - } else { - ForestKind::Savannah - } - } else if humidity > CONFIG.jungle_hum { - // Temperate climate with jungle humidity... - // https://en.wikipedia.org/wiki/Humid_subtropical_climates are often - // densely wooded and full of water. Semitropical rainforests, basically. - // For now we just treat them like other rainforests. - ForestKind::Oak - } else if humidity > CONFIG.forest_hum { - // Moderate climate, moderate humidity. - ForestKind::Oak - } else if humidity > CONFIG.desert_hum { - // With moderate temperature and low humidity, we should probably see - // something different from savannah, but oh well... - ForestKind::Savannah - } else { - ForestKind::Savannah - } - } else { - // For now we don't take humidity into account for cold climates (but we really - // should!) except that we make sure we only have snow pines when there is snow. - if temp <= CONFIG.snow_temp { - ForestKind::SnowPine - } else if humidity > CONFIG.desert_hum { - ForestKind::Pine - } else { - // Should really have something like tundra. - ForestKind::Pine - } + forest_kind: { + // Whittaker diagram + let candidates = [ + // A smaller prevalence means that the range of values this tree appears in + // will shrink compared to neighbouring trees in the + // topology of the Whittaker diagram. + // Humidity, temperature, near_water, each with prevalence + ( + ForestKind::Palm, + (CONFIG.desert_hum, 1.5), + (CONFIG.tropical_temp, 1.5), + (1.0, 2.0), + ), + ( + ForestKind::Savannah, + (CONFIG.desert_hum, 2.0), + (CONFIG.tropical_temp, 1.5), + (0.0, 1.0), + ), + ( + ForestKind::Oak, + (CONFIG.forest_hum, 1.5), + (0.0, 1.5), + (0.0, 1.0), + ), + ( + ForestKind::Mangrove, + (CONFIG.jungle_hum, 0.5), + (CONFIG.tropical_temp, 0.5), + (0.0, 1.0), + ), + ( + ForestKind::Pine, + (CONFIG.forest_hum, 1.25), + (CONFIG.snow_temp, 2.5), + (0.0, 1.0), + ), + ( + ForestKind::Birch, + (CONFIG.desert_hum, 1.5), + (CONFIG.temperate_temp, 1.5), + (0.0, 1.0), + ), + ]; + + candidates + .iter() + .enumerate() + .min_by_key(|(i, (_, (h, h_prev), (t, t_prev), (w, w_prev)))| { + let rand = RandomPerm::new(*i as u32 * 1000); + let noise = + Vec3::iota().map(|e| (rand.get(e) & 0xFF) as f32 / 255.0 - 0.5) * 2.0; + (Vec3::new( + (*h - humidity) / *h_prev, + (*t - temp) / *t_prev, + (*w - if river.near_water() { 1.0 } else { 0.0 }) / *w_prev, + ) + .add(noise * 0.1) + .map(|e| e * e) + .sum() + * 10000.0) as i32 + }) + .map(|(_, c)| c.0) + .unwrap() // Can't fail }, spawn_rate: 1.0, river, diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 022f33bfb4..7142469469 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -93,7 +93,7 @@ pub fn center_of(p: [Vec2; 3]) -> Vec2 { impl WorldSim { fn can_host_settlement(&self, pos: Vec2) -> bool { self.get(pos) - .map(|chunk| !chunk.near_cliffs && !chunk.river.is_river() && !chunk.river.is_lake()) + .map(|chunk| !chunk.river.is_river() && !chunk.river.is_lake()) .unwrap_or(false) && self .get_gradient_approx(pos)