From 9d8fab20568c5dabd5592644ba0a6bc8241af051 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Wed, 4 Nov 2020 00:30:47 +0000 Subject: [PATCH 01/10] Switched path generation to canvas --- world/src/canvas.rs | 63 ++++++++++++++++++++++++++++++++++++++++++ world/src/layer/mod.rs | 36 ++++++++++++------------ world/src/lib.rs | 21 ++++++++++++-- 3 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 world/src/canvas.rs diff --git a/world/src/canvas.rs b/world/src/canvas.rs new file mode 100644 index 0000000000..e1c1b07862 --- /dev/null +++ b/world/src/canvas.rs @@ -0,0 +1,63 @@ +use vek::*; +use common::{ + terrain::{TerrainChunk, Block, TerrainChunkSize}, + vol::{ReadVol, WriteVol, RectVolSize}, +}; +use crate::{ + block::ZCache, + util::Grid, + column::ColumnSample, + index::IndexRef, +}; + +pub struct CanvasInfo<'a> { + pub(crate) wpos: Vec2, + pub(crate) column_grid: &'a Grid>>, + pub(crate) column_grid_border: i32, + pub(crate) index: IndexRef<'a>, +} + +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<&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 { + self.index + } +} + +pub struct Canvas<'a> { + pub(crate) wpos: Vec2, + pub(crate) chunk: &'a mut TerrainChunk, +} + +impl<'a> Canvas<'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 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); + } +} diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 666664f49c..61dacb4efa 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -6,6 +6,8 @@ use crate::{ column::ColumnSample, util::{RandomField, Sampler}, IndexRef, + Canvas, + CanvasInfo, }; use common::{ assets::Asset, @@ -33,19 +35,17 @@ 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, + canvas: &mut Canvas, + info: &CanvasInfo, ) { - for y in 0..vol.size_xy().y as i32 { - for x in 0..vol.size_xy().x as i32 { + for y in 0..canvas.area().size().h as i32 { + for x in 0..canvas.area().size().w as i32 { let offs = Vec2::new(x, y); - let wpos2d = wpos2d + offs; + let wpos2d = canvas.wpos() + offs; // Sample terrain - let col_sample = if let Some(col_sample) = get_column(offs) { + let col_sample = if let Some(col_sample) = info.col(wpos2d) { col_sample } else { continue; @@ -70,10 +70,10 @@ pub fn apply_paths_to<'a>( // 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 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)) }; @@ -96,12 +96,12 @@ pub fn apply_paths_to<'a>( 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), + 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(index.colors.layer.bridge.into(), 8), + noisy_color(info.index().colors.layer.bridge.into(), 8), ) } else { let path_color = path.surface_color( @@ -113,9 +113,9 @@ pub fn apply_paths_to<'a>( } 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 pos = Vec3::new(wpos2d.x, wpos2d.y, surface_z + z); + if canvas.get(pos).unwrap().kind() != BlockKind::Water { + let _ = canvas.set(pos, EMPTY_AIR); } } } diff --git a/world/src/lib.rs b/world/src/lib.rs index 7aa02fb46c..d52a39c3c1 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -13,6 +13,7 @@ mod all; mod block; +pub mod canvas; pub mod civ; mod column; pub mod config; @@ -25,7 +26,10 @@ pub mod site; pub mod util; // Reexports -pub use crate::config::CONFIG; +pub use crate::{ + config::CONFIG, + canvas::{Canvas, CanvasInfo}, +}; pub use block::BlockGen; pub use column::ColumnSample; pub use index::{IndexOwned, IndexRef}; @@ -126,7 +130,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( @@ -203,7 +206,19 @@ impl World { // 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 canvas_info = CanvasInfo { + wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + column_grid: &zcache_grid, + column_grid_border: grid_border, + index, + }; + let mut canvas = Canvas { + wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + chunk: &mut chunk, + }; + + layer::apply_paths_to(&mut canvas, &canvas_info); // Apply site generation sim_chunk.sites.iter().for_each(|site| { From 27821c3aedb1de021d04ed5af912b84b8fa542ff Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 7 Nov 2020 21:50:52 +0000 Subject: [PATCH 02/10] Turned tree generation into a post-processing layer, ripped out old tree generator for performance wins --- .../shaders/include/cloud/regular.glsl | 2 +- server/src/lib.rs | 4 +- world/src/block/mod.rs | 475 ++++-------------- world/src/block/natural.rs | 91 ---- world/src/canvas.rs | 77 ++- world/src/column/mod.rs | 60 --- world/src/layer/mod.rs | 297 ++++++----- world/src/layer/scatter.rs | 136 +++-- world/src/layer/tree.rs | 127 +++++ world/src/lib.rs | 36 +- world/src/sim/mod.rs | 26 +- 11 files changed, 506 insertions(+), 825 deletions(-) delete mode 100644 world/src/block/natural.rs create mode 100644 world/src/layer/tree.rs 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/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/block/mod.rs b/world/src/block/mod.rs index 0896dd7f79..d19ab5569f 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,133 @@ 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); + 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, + ) + .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); + }) + .or_else(|| { + // Water + if (wposf.z as f32) < water_height { + // Ocean + Some(water) + } else { + None + } + }); block } } 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 index e1c1b07862..d6ed1acd14 100644 --- a/world/src/canvas.rs +++ b/world/src/canvas.rs @@ -1,32 +1,39 @@ -use vek::*; -use common::{ - terrain::{TerrainChunk, Block, TerrainChunkSize}, - vol::{ReadVol, WriteVol, RectVolSize}, -}; use crate::{ block::ZCache, - util::Grid, 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 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() + Rect::from(( + self.wpos(), + Extent2::from(TerrainChunkSize::RECT_SIZE.map(|e| e as i32)), + )) + .into() } - pub fn col(&self, pos: Vec2) -> Option<&ColumnSample> { + pub fn col(&self, pos: Vec2) -> Option<&'a ColumnSample> { self.column_grid .get(self.column_grid_border + pos - self.wpos()) .map(Option::as_ref) @@ -34,24 +41,24 @@ impl<'a> CanvasInfo<'a> { .map(|zc| &zc.sample) } - pub fn index(&self) -> IndexRef { - self.index - } + 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) wpos: Vec2, + pub(crate) info: CanvasInfo<'a>, pub(crate) chunk: &'a mut TerrainChunk, } impl<'a> Canvas<'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() - } + /// 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() @@ -60,4 +67,30 @@ impl<'a> Canvas<'a> { 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/column/mod.rs b/world/src/column/mod.rs index 665b98fe86..73bdd597ce 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> { @@ -1053,7 +1002,6 @@ 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, @@ -1086,7 +1034,6 @@ 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, @@ -1104,10 +1051,3 @@ pub struct ColumnSample<'a> { pub chunk: &'a SimChunk, } - -#[derive(Copy, Clone)] -pub struct StructureData { - pub pos: Vec2, - pub seed: u32, - pub meta: Option, -} diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 61dacb4efa..652bea0df0 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -1,13 +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, - CanvasInfo, + Canvas, IndexRef, }; use common::{ assets::Asset, @@ -34,178 +33,162 @@ pub struct Colors { const EMPTY_AIR: Block = Block::air(SpriteKind::Empty); -pub fn apply_paths_to<'a>( - canvas: &mut Canvas, - info: &CanvasInfo, -) { - for y in 0..canvas.area().size().h as i32 { - for x in 0..canvas.area().size().w as i32 { - let offs = Vec2::new(x, y); +pub fn apply_paths_to<'a>(canvas: &mut Canvas) { + let info = canvas.info(); + canvas.foreach_col(|canvas, wpos2d, col| { + let surface_z = col.riverless_alt.floor() as i32; - let wpos2d = canvas.wpos() + 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) = info.col(wpos2d) { - 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 = 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 [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 _ = 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_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(wpos2d.x, wpos2d.y, surface_z + z); - if canvas.get(pos).unwrap().kind() != BlockKind::Water { - let _ = canvas.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<'a>(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..d5a533b7f8 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<'a>(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..b016dcb31e --- /dev/null +++ b/world/src/layer/tree.rs @@ -0,0 +1,127 @@ +use crate::{ + all::ForestKind, + block::block_from_structure, + column::ColumnGen, + util::{RandomPerm, Sampler, UnitChooser}, + Canvas, CONFIG, +}; +use common::{ + terrain::{ + structure::{Structure, StructureBlock}, + Block, + }, + 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 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"); +} + +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<'a>(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::Palm => &PALMS, + ForestKind::Savannah => &ACACIAS, + ForestKind::Oak if QUIRKY_RAND.get(seed) % 16 == 7 => &OAK_STUMPS, + ForestKind::Oak if QUIRKY_RAND.get(seed) % 19 == 7 => &FRUIT_TREES, + ForestKind::Oak if QUIRKY_RAND.get(seed) % 14 == 7 => &BIRCHES, + ForestKind::Oak => &OAKS, + ForestKind::Pine => &PINES, + ForestKind::SnowPine => &SNOW_PINES, + ForestKind::Mangrove => &MANGROVE_TREES, + } + }; + models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize % models.len()] + .clone() + }, + seed, + units: UNIT_CHOOSER.get(seed), + }) + }) { + tree + } else { + continue; + }; + + let bounds = tree.model.get_bounds(); + for z in bounds.min.z..bounds.max.z { + 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(), + tree.model + .get(model_pos) + .ok() + .copied() + .unwrap_or(StructureBlock::None), + wpos, + tree.pos.xy(), + tree.seed, + col, + Block::air, + ) + .map(|block| canvas.set(wpos, block)); + } + } + }); +} diff --git a/world/src/lib.rs b/world/src/lib.rs index d52a39c3c1..9f6812dfed 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -8,7 +8,8 @@ const_generics, const_panic, label_break_value, - or_patterns + or_patterns, + array_value_iter )] mod all; @@ -27,8 +28,8 @@ pub mod util; // Reexports pub use crate::{ - config::CONFIG, canvas::{Canvas, CanvasInfo}, + config::CONFIG, }; pub use block::BlockGen; pub use column::ColumnSample; @@ -171,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); @@ -181,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); } }); @@ -204,21 +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); - - let canvas_info = CanvasInfo { - wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), - column_grid: &zcache_grid, - column_grid_border: grid_border, - index, - }; let mut canvas = Canvas { - wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + 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_paths_to(&mut canvas, &canvas_info); + 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..5adf44d64d 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -1431,25 +1431,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 +1983,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)] From 3f7dba51270360c873c95e02d08cc61fdd14e2c7 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 7 Nov 2020 22:51:12 +0000 Subject: [PATCH 03/10] Abandon tree generation when outside bounds --- world/src/layer/tree.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs index b016dcb31e..520d2690c6 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -7,7 +7,7 @@ use crate::{ }; use common::{ terrain::{ - structure::{Structure, StructureBlock}, + structure::Structure, Block, }, vol::ReadVol, @@ -109,11 +109,16 @@ pub fn apply_trees_to<'a>(canvas: &mut Canvas) { ) + Vec3::unit_z() * (wpos.z - tree.pos.z); block_from_structure( info.index(), - tree.model + if let Some(block) = tree.model .get(model_pos) .ok() .copied() - .unwrap_or(StructureBlock::None), + { + 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, From 69beba3e79c3875cb334999572a679ea00be55f0 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 8 Nov 2020 23:19:07 +0000 Subject: [PATCH 04/10] Removed more redundant code, desert dunes, better bridges --- assets/voxygen/item_image_manifest.ron | 4 +- world/src/block/mod.rs | 6 +- world/src/civ/mod.rs | 6 +- world/src/column/mod.rs | 118 +++++++++++++------------ world/src/layer/mod.rs | 4 +- world/src/layer/scatter.rs | 2 +- world/src/layer/tree.rs | 22 ++--- world/src/sim/mod.rs | 27 +++--- world/src/site/settlement/mod.rs | 2 +- 9 files changed, 96 insertions(+), 95 deletions(-) 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/world/src/block/mod.rs b/world/src/block/mod.rs index d19ab5569f..53ddc1cc73 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -107,7 +107,7 @@ impl<'a> BlockGen<'a> { 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 { + 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, @@ -169,9 +169,7 @@ impl<'a> BlockGen<'a> { } else { None } - }); - - block + }) } } 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 73bdd597ce..62c21f5b48 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -405,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) @@ -440,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), @@ -457,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(|| { @@ -642,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) @@ -675,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, ) }, @@ -686,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, )) }); @@ -704,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, ) }; @@ -729,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.1), + 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) @@ -953,7 +965,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 { @@ -1005,10 +1017,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { marble, marble_small, rock, - is_cliffs, - near_cliffs, - cliff_hill, - close_cliffs: sim.gen_ctx.cliff_gen.get(wpos), temp, humidity, spawn_rate, @@ -1037,10 +1045,6 @@ pub struct ColumnSample<'a> { 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, diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 652bea0df0..77b9b1e1e0 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -33,7 +33,7 @@ pub struct Colors { const EMPTY_AIR: Block = Block::air(SpriteKind::Empty); -pub fn apply_paths_to<'a>(canvas: &mut Canvas) { +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; @@ -106,7 +106,7 @@ pub fn apply_paths_to<'a>(canvas: &mut Canvas) { }); } -pub fn apply_caves_to<'a>(canvas: &mut Canvas) { +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; diff --git a/world/src/layer/scatter.rs b/world/src/layer/scatter.rs index d5a533b7f8..d5e74015d3 100644 --- a/world/src/layer/scatter.rs +++ b/world/src/layer/scatter.rs @@ -8,7 +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>(canvas: &mut Canvas) { +pub fn apply_scatter_to(canvas: &mut Canvas) { use SpriteKind::*; #[allow(clippy::type_complexity)] // TODO: Add back all sprites we had before diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs index 520d2690c6..66f64def68 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -6,10 +6,7 @@ use crate::{ Canvas, CONFIG, }; use common::{ - terrain::{ - structure::Structure, - Block, - }, + terrain::{structure::Structure, Block}, vol::ReadVol, }; use lazy_static::lazy_static; @@ -34,7 +31,7 @@ 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<'a>(canvas: &mut Canvas) { +pub fn apply_trees_to(canvas: &mut Canvas) { struct Tree { pos: Vec3, model: Arc, @@ -84,8 +81,10 @@ pub fn apply_trees_to<'a>(canvas: &mut Canvas) { ForestKind::Mangrove => &MANGROVE_TREES, } }; - models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize % models.len()] - .clone() + Arc::clone( + &models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize + % models.len()], + ) }, seed, units: UNIT_CHOOSER.get(seed), @@ -109,14 +108,11 @@ pub fn apply_trees_to<'a>(canvas: &mut Canvas) { ) + Vec3::unit_z() * (wpos.z - tree.pos.z); block_from_structure( info.index(), - if let Some(block) = tree.model - .get(model_pos) - .ok() - .copied() - { + 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. + // If we hit an inaccessible block, we're probably outside the model bounds. + // Skip this column. break; }, wpos, diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 5adf44d64d..fb5696f748 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -103,7 +103,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 +111,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 +501,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 +511,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) @@ -2004,8 +2000,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, @@ -2095,10 +2089,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))) @@ -2165,8 +2155,23 @@ impl SimChunk { .sub(0.5) .mul(0.95) .add(0.5) + * (1.0 - temp as f64) } as f32; + // 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, flux, @@ -2185,8 +2190,6 @@ 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 { 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) From 4944eb5f59aa7b4cb9cfab7e36088f368aae4349 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 9 Nov 2020 15:06:37 +0000 Subject: [PATCH 05/10] Better humidity, better snow trees --- common/src/terrain/block.rs | 1 + world/src/all.rs | 2 +- world/src/column/mod.rs | 12 +++-- world/src/config.rs | 2 +- world/src/layer/tree.rs | 23 +++++++-- world/src/sim/mod.rs | 97 ++++++++++++++++++++++++++++++------- 6 files changed, 108 insertions(+), 29 deletions(-) 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/world/src/all.rs b/world/src/all.rs index 5c9e4fbe4c..3020c4711f 100644 --- a/world/src/all.rs +++ b/world/src/all.rs @@ -4,6 +4,6 @@ pub enum ForestKind { Savannah, Oak, Pine, - SnowPine, + // SnowPine, Mangrove, } diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 62c21f5b48..466987a82a 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -733,7 +733,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { ); // Columns near water get a humidity boost let humidity = Lerp::lerp( - Lerp::lerp(humidity, 1.0, 0.1), + Lerp::lerp(humidity, 1.0, 0.25), humidity, water_dist .map(|water_dist| water_dist / 20.0) @@ -879,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. @@ -947,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 @@ -1024,6 +1026,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { water_dist, path, cave, + snow_cover, chunk: sim_chunk, }) @@ -1052,6 +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, } 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/tree.rs b/world/src/layer/tree.rs index 66f64def68..0a2505db18 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -6,7 +6,7 @@ use crate::{ Canvas, CONFIG, }; use common::{ - terrain::{structure::Structure, Block}, + terrain::{structure::Structure, Block, BlockKind}, vol::ReadVol, }; use lazy_static::lazy_static; @@ -18,7 +18,7 @@ lazy_static! { 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 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"); @@ -77,7 +77,7 @@ pub fn apply_trees_to(canvas: &mut Canvas) { ForestKind::Oak if QUIRKY_RAND.get(seed) % 14 == 7 => &BIRCHES, ForestKind::Oak => &OAKS, ForestKind::Pine => &PINES, - ForestKind::SnowPine => &SNOW_PINES, + // ForestKind::SnowPine => &SNOW_PINES, ForestKind::Mangrove => &MANGROVE_TREES, } }; @@ -96,7 +96,8 @@ pub fn apply_trees_to(canvas: &mut Canvas) { }; let bounds = tree.model.get_bounds(); - for z in bounds.min.z..bounds.max.z { + let mut is_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) @@ -121,7 +122,19 @@ pub fn apply_trees_to(canvas: &mut Canvas) { col, Block::air, ) - .map(|block| canvas.set(wpos, block)); + .map(|block| { + if is_top && col.snow_cover && block.kind() == BlockKind::Leaves { + canvas.set( + wpos + Vec3::unit_z(), + Block::new(BlockKind::Snow, Rgb::new(210, 210, 255)), + ); + } + canvas.set(wpos, block); + is_top = false; + }) + .unwrap_or_else(|| { + is_top = true; + }); } } }); diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index fb5696f748..f951f991b6 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -2056,11 +2056,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]; @@ -2075,6 +2070,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); @@ -2138,7 +2145,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); @@ -2149,14 +2155,17 @@ 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) - * (1.0 - temp as f64) } as f32; + const MIN_TREE_HUM: f32 = 0.05; + // 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 @@ -2191,23 +2200,74 @@ impl SimChunk { 0.0 }, tree_density, - forest_kind: if temp > CONFIG.temperate_temp { - if temp > CONFIG.desert_temp { - if humidity > CONFIG.jungle_hum { + 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, 0.5), + (1.0, 2.0), + ), + ( + ForestKind::Savannah, + (CONFIG.desert_hum, 1.5), + (CONFIG.tropical_temp, 1.0), + (0.0, 1.0), + ), + ( + ForestKind::Oak, + (CONFIG.forest_hum, 1.5), + (CONFIG.temperate_temp, 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.desert_hum, 2.0), + (CONFIG.snow_temp, 2.5), + (0.0, 1.0), + ), + ]; + + candidates + .iter() + .min_by_key(|(_, (h, h_prev), (t, t_prev), (w, w_prev))| { + (Vec3::new( + (*h - humidity) / *h_prev, + (*t - temp) / *t_prev, + (*w - if river.near_water() { 1.0 } else { 0.0 }) / *w_prev, + ) + .map(|e| e * e) + .sum() + * 10000.0) as i32 + }) + .map(|c| c.0) + .unwrap() // Can't fail + }, + /* + if temp > CONFIG.temperate_temp { + if temp > CONFIG.tropical_temp { + if humidity > CONFIG.desert_hum && river.near_water() { // 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 { + } else { // Low but not desert humidity, so we should really have some other // terrain... ForestKind::Savannah - } else { - ForestKind::Savannah } - } else if temp > CONFIG.tropical_temp { + } else if temp > CONFIG.forest_hum { if humidity > CONFIG.jungle_hum { if tree_density > 0.0 { // println!("Mangrove: {:?}", wposf); @@ -2250,6 +2310,7 @@ impl SimChunk { ForestKind::Pine } }, + */ spawn_rate: 1.0, river, warp_factor: 1.0, From de78c64107681cd01ca6d1071996d9e8b61778d7 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 9 Nov 2020 16:22:47 +0000 Subject: [PATCH 06/10] Better pine forests by shifting average oak temperature --- world/src/sim/mod.rs | 75 ++++++++------------------------------------ 1 file changed, 13 insertions(+), 62 deletions(-) diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index f951f991b6..2cd1dca0b3 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::{ @@ -2161,7 +2164,7 @@ impl SimChunk { .sub(0.5) .add(0.5) } as f32; - const MIN_TREE_HUM: f32 = 0.05; + 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) @@ -2222,7 +2225,7 @@ impl SimChunk { ( ForestKind::Oak, (CONFIG.forest_hum, 1.5), - (CONFIG.temperate_temp, 1.5), + (0.0, 1.5), (0.0, 1.0), ), ( @@ -2241,76 +2244,24 @@ impl SimChunk { candidates .iter() - .min_by_key(|(_, (h, h_prev), (t, t_prev), (w, w_prev))| { + .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) + .map(|(_, c)| c.0) .unwrap() // Can't fail }, - /* - if temp > CONFIG.temperate_temp { - if temp > CONFIG.tropical_temp { - if humidity > CONFIG.desert_hum && river.near_water() { - // Forests in desert temperatures with extremely high humidity - // should probably be different from palm trees, but we use them - // for now. - ForestKind::Palm - } else { - // Low but not desert humidity, so we should really have some other - // terrain... - ForestKind::Savannah - } - } else if temp > CONFIG.forest_hum { - 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 - } - }, - */ spawn_rate: 1.0, river, warp_factor: 1.0, From ed41d0fa9becea043abdc52ca310757e97843965 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 9 Nov 2020 16:28:57 +0000 Subject: [PATCH 07/10] Reduced oaks in desert --- world/src/sim/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 2cd1dca0b3..8a9383e7c2 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -2218,8 +2218,8 @@ impl SimChunk { ), ( ForestKind::Savannah, - (CONFIG.desert_hum, 1.5), - (CONFIG.tropical_temp, 1.0), + (CONFIG.desert_hum, 2.0), + (CONFIG.tropical_temp, 1.5), (0.0, 1.0), ), ( From 81a01b4740949ff663bc149b0dcb6eaf660a742e Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 9 Nov 2020 17:09:33 +0000 Subject: [PATCH 08/10] Better snow effects for non-trees --- world/src/layer/tree.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs index 0a2505db18..2cba78e938 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -97,6 +97,7 @@ pub fn apply_trees_to(canvas: &mut Canvas) { 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( @@ -123,17 +124,22 @@ pub fn apply_trees_to(canvas: &mut Canvas) { Block::air, ) .map(|block| { - if is_top && col.snow_cover && block.kind() == BlockKind::Leaves { + // 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_top = true; + is_leaf_top = true; }); } } From 1b03d0437c48bca4c643564d9c77744c4f9628be Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 9 Nov 2020 17:19:29 +0000 Subject: [PATCH 09/10] Updated changelog, removed old models --- CHANGELOG.md | 3 +++ assets/voxygen/element/map/dungeon.png | Bin 0 -> 932 bytes assets/voxygen/element/map/town.png | Bin 0 -> 1096 bytes assets/world/manifests/snow_pines.ron | 36 ------------------------- assets/world/tree/snow_pine/1.vox | Bin 11264 -> 0 bytes assets/world/tree/snow_pine/2.vox | Bin 17608 -> 0 bytes assets/world/tree/snow_pine/3.vox | Bin 20988 -> 0 bytes assets/world/tree/snow_pine/4.vox | Bin 7064 -> 0 bytes assets/world/tree/snow_pine/5.vox | Bin 10732 -> 0 bytes assets/world/tree/snow_pine/6.vox | Bin 7776 -> 0 bytes assets/world/tree/snow_pine/7.vox | Bin 15544 -> 0 bytes assets/world/tree/snow_pine/8.vox | Bin 9820 -> 0 bytes world/src/layer/tree.rs | 12 +++++---- 13 files changed, 10 insertions(+), 41 deletions(-) create mode 100644 assets/voxygen/element/map/dungeon.png create mode 100644 assets/voxygen/element/map/town.png delete mode 100644 assets/world/manifests/snow_pines.ron delete mode 100644 assets/world/tree/snow_pine/1.vox delete mode 100644 assets/world/tree/snow_pine/2.vox delete mode 100644 assets/world/tree/snow_pine/3.vox delete mode 100644 assets/world/tree/snow_pine/4.vox delete mode 100644 assets/world/tree/snow_pine/5.vox delete mode 100644 assets/world/tree/snow_pine/6.vox delete mode 100644 assets/world/tree/snow_pine/7.vox delete mode 100644 assets/world/tree/snow_pine/8.vox 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 0000000000000000000000000000000000000000..b4a78b954a5be0df05f5e986691fe0b081004c42 GIT binary patch literal 932 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 diff --git a/assets/world/tree/snow_pine/2.vox b/assets/world/tree/snow_pine/2.vox deleted file mode 100644 index 12d259ef95913b57fb529909b5eda4f5d4c31c60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17608 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 X>7TuQ+durl+eiQ8gWr>rp8)?GO4t9R diff --git a/assets/world/tree/snow_pine/3.vox b/assets/world/tree/snow_pine/3.vox deleted file mode 100644 index bbda6299d8c83e4b339c62df21ef604fb5d1a6c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20988 zcmd7a%d0KVeh~0|_TKB&uj;Dm$LjUm`<(MTzvsOsJ`#kOL`e+Sn3(v8FbV@pt)9Ro|+vuC9Jn zuYI^b@yCAh>6h;A?tbJCe(OgicdtKwclRg0^;19Cl!E_0;fe3yC;!Y(ee2)+h>ZV! zF5lg?_1(uhy!n`8^OF1eCB>q7NqtfBm9%=v()@K=OSO%yv{g^ve$0Ik3-ZM9F~v^4 z6GNK%Uc8s@?MuFo@==<^auZ8_r`1c`ri2OU)tf`c*|IoU-lY zV^ub;rPba(r=*>HZ*F?&v6RdHqkWpUT-_V5KFwKYe|y_k#@)5|&i%bH-y5%O%tx2+ zUpixq%>$1gW4qGN_N7l7)-NHg9M9v&Hb11^K;j4SAGcI>@*nFe!ZK1TD>+i$0z z;P^Z9-t~_kgJTZ;+VQy-99t$jj^z9#{iSl=jlDO2UGv~RUw@3|r#C;nF$VJ!%ujGG zf^(Alo$Yt-dD70g-8nDvL7qX~pe_~iUAfo$FYiyyeE$&g{Ua;ZY`E|9{R5AzShHcv zUCj4yxaWaKR;<~u7-0gU^vG6Od`?~=ok*_NJM%99WCRc4VgrwC(zL{?zJJ4i1Y+H zTFxF^C(fOevxn8GXsD^!#rq9wR-CrVtXY-(MxGUqY}U%Gc;tc2N{khcJg|8bW5pv6 zY#zi|@yG+4dofl#vTNu2ikgO&jzG_lm-`_t_fbBP**>QG9TlVU#9ZPlaVlyW_TmeN zQomMDMNPv|{6y8M->9czyOpP+R@cy$Hal@D_R0$fjtsNyD)m%k(6Ou4)5>Qr zi$tU+(2+;&8tq8(k)A+D9>j@MtvYF-BWHC)dIB9)ms}U-IyfG>$~@B(X~p&|95`~K z>D94k;lPm-`zXeNBPZ&9xv%1KU(*b>r5@Sj`-Zlp%58~l>8n_;Us>hZuCVWh|+Ku;u*85o&pY}>M3^hZTaQ&J0uhL(;%Pb85U7?}v3*FBL$ zW?-cAybkn460Lm<(vi-yI?xkIWClh$&*?xBWyBdG2|(F}R;c2KW5P;Mp)1o*ie;?+aI+ zID5Wdxbnm^XV3i$SDtw0E$?{G*}LJwl_#Eg%RAn4@&35-#4~Ss$9pc`F;|{==HlIR z<%#Gx5}ARKiJ2}qetIH_%)rRROxJ5iPh?{bDh`XQo$Hi|Jb zkx4{)0v*es9WxUn1DQmmC(tqLJ0$&Q+c7gSGLT6`dIDX^+a@wH5%S)8WwdS?tzV2I z>5rL-k%3Gi(i7-P-u971W@IbAVa*ct$IQgYQ1aS!Lzz2$5a>!O)|WVmOg9=QJxi}Y zW+ujx_Z}IfQGFuH_XIk|V4r06+9qkFCopx|FpybF8)>8`Ftyq+kV!;(YU_o{dZ8xU ze>WLNqn@6@JE_tpi1*Z+qi4Q)dfISuFP_|!C->&b^?LHWIeFfkJa0~(H)7iCSxQ@4 zZ5j@(7~MgPhMJ1gVYokXV6V+g%h^Zfi6aLV_B5qUZ$F&(+HmB+!oH;H4yArp&yfS? zNtq)DDt4SlagH2lX{f2#aUQhc$bpuInu=Y?8{09fr=_8$V#lE|59~Q6Wm+0)Dt653 zCq@o&@SZ$5SLQ{&p{8O-6z}PllXtANw4>qB>z{_2ie1U8+Xdy)rq(`T`-Yl|MSRc9 zL?Y4`yWu`CZHN1jflMOO6PP#JFfx!yM0x`AS{p_NGKok}DD1PNr75yJwS2{)GG6w~ zOuSw4{WDKox$t%`#uHaAr93NhYL|6x3@pG-ud&&x`2^^Od`?~m>;!a zWFV7>^h^)x7|0|dJ=489%*5CKvugDV^!Snw}Cm%R+D#Ys2Uu_#|8d^F6J&{CaX3xUT_6O;as`SOF zbo%Ni3(G#P-67q>-Wnap<`P_B#x$EU~*UjgyLFa8adzZhv zpGo;M+kB>v&#d!4v(0D5EXJgMWGLci*1YfRM_Yzo8~P%AW*nbdGk$8#_^CCcytY+* z=Y9Iln(k9;x=+1d?K5h_NEAy1`Hs0)$4EDd7bVXe)G^YFM-olezgR~t-z58_qU+y# zN56NC>8BiX{9tYSK|k+}$2pzf`=)vCHxzBea;v>I4TJp1#7xvaL~~ivWG=~_Z5f%^ z?T+T*bI(t6FC7?})wTMcC+*0CIy!UQ(v4^DTq&dafi@ZwL0wN|w>^4hJAacgko^ui zkNTud`i)=uPX5vwQ-9W&4HY|bIj*&9SK4Vi+GZ%*4z|tq#l#psai4tRI{n0T`iXn# z6X*96b4)K!WFSOkA_E~Q6UhvWC7;;NlONoZ#z`I2 z(UK^Q#=_TPbmnCuS+5 zIh{!Qj@?(r{FP(;%DdLwdKMl$zq7ht?BX6j7w)s6|H?bVbHw#<`p5eLR`RCuRMIDOdG}LtM!_FN=MpWtdtp<2y5dfl9;rc zOWUBYPJ6L|vDA_Ki);5{?k~=p`n`QKGBFdxJ6cx0_1A3Jvg6Tr{i=N5x3*zqAd^^^ zy5{Qd2ex5X(&pmt2)1R#q<&-|li1hx&&0?;W?zXjF*1-@>u1B3T~YSoJN}UsYc_1z zm9)M1t@`3`5b9a8Vatw6T}{K<_w=TGd$Z$2Xy3Y~-@5+aI?lJ|Nts9@m-5E7MLoNY zcFsKw_4H&dzqdSY&w?PVSkxdv<+1`^NPCR$kmAmE)}(<3aw& zsc_#NIk1$pv}JGG{Xif;du}{?AIbM5;AZBO{d(JnEXU*n~tRaNKc?+5^s&Kqg8J8-4TtaC(zLrlRSZ*);K!? ztz+m2E z8oyVy>{+Nwtnm9*%W<|13;RNB;ZW+RJg;jSzj?LnSvYW{8uZC=5@TV{TvG9cL#Z!D zF%}Ml`rz6>xCajIg@fnq!E^WE`Fn5=sUwoe42*H z46l!@ShK-{^Ywv8R;<~uBzZxy#z%f$;jk zBP-Ty*y4fl`oJSA)@<1FIEt}m!?u`)*K|p*k)A+0E7KE5CuMp9!!f;P5|N%j$8bS^Wkqz#dtKt~?6YqTTDM|uJsc@QU3wd$mSj-1sI=?QdH zo#Wn)uMPEXeC?|JYoI66h_yvk>PB_)wLF2$z{o_~s3VXW7?}v|=zO@B)@%mzI=t32 zw57Zq%&iy=EgjWPjE0tuNFp;ZGErCB(b5se_a+bOd@LiOj&r#LS+B z14mArsT+OK(h=y1Br*dd6Ek}j4jeggrf&5`OGlt5lE@5XLX<_lE@5yPYE?jxync4lgXW_t+Q%QqoiTibAa6gX>?)j0yvtcYeJIU_I$r^<%ws`p8FTBJn_t1-tnHZcf*A%PdxLMcf9A~{c+`qXWsIT_guVVt~~L~ z#k=Rq6Z0^?PK?Z1o{>mTpkq$z7|A3eJ%Nrfs^h#&ujf7U^m^jR!k(GaB*uY-Ju?%B zQH(t^6PZM$C(yAB+A%XRGLT6`dIB9S4NJBiGZP~NnM9-~(9zORQ?X;qhBZslA2Smp z1DQmmC(zN-P*bsE%Z4>e)E_ewBLkU4q$kkP(oj>eW6Op$ORqm>CdQKY9vP&WM5HIs zF$Vi2v)49BBRzqs(}sb}Qrbu(J%Op!hJj2X(i4~(^$cVZk)FU*t7jmSi1Y-8N}NPw z+=-D%M8>TcnZ&qJCX*P~%48C!!}xmSz|%6mUb%4Q#E}CFd!F{%aN*2}BL^1tTxa#1 zIdSAr%9Aoj4pi(okK!CT(9%#-vEw{w!;u3m4K)=zwj8s1S{iC9cC2{hfn(B+mWG;& z9V;Gr;1I1LNVGK6RP5NYVa!G&|4K)=zwj~YLBi1n^1DQmmXC6I&ta(NT zGKok}V6t8s8OS6eJ%M?n4I=}YM5HG$ueD)hAd`sn1jdy*GKt9eC`KkRJ}8q(yj_OZ zXP&rn;q6|GC$3yd**o!xE0>b@&b)FdsrTuHy}4M}dgss9ZWAK|nM9-~Fh6R;$Ur6$ z>6sqXF_1|_dZv4I_!k1N{<8h*Uwgg!SHG`4;~Q}@sie+hVm}!lxwpQ(6K7!La&R0x z@yuH;M=_pw<}H_#7*9O&mdja;C!R}s;c6Y_UcGQF_bktxj_whjc;<9crdrLuIn3R9 z_Kh+7#!#lAWxEllqNbr`y`6o3sprgvE9;#&Th3g#mU5-cg)2{N>)EqRoC{Z;cxGtC zxp3u)p%vr8l_yeXUKu!Z;YteP44k=eCH3Vzh;yd%eh&0R65IaZUOdPj+`|X=uynh( z?%!MY``)1C#CEs$U8RnihL(fp|B+5UaO6~oo!Qm4p{AjwBhV8`WM=j(>}-3G9;xJO zPV#3iT-i11sA<%CW~|LmkX5JGN|Cv!d?Qv17}Ib;-B#tl2j5tl6EH*Xp#q?$}ZvmD#Z+92UP-EPkt4 zUK?sEcJzyS&hi(|+Rw+U-z~JKp{AnWYfDE|}ZJD2a>KkuDaw)xyTPmD?Z$WX-3t$E9Sv}NeE zp)bPc)|a1IGk#{x_?b1Mytb8b)r8?Q*WhQ~ul54(b@` z#UqKv+y=3ZTD~#gH5HxXZ0WkstU-%Ut!+Qm&nNcd_~uW1(|qDL6m7(EtGzZ2gZ#+E zTsYk0Tlx+vwX8U4d3?Dpq zKiJO)`}|;?{K5JCV2Q3ZzP@stU%3vxax7n3 z<9}t1|CP18lsL!}vy{<1PGv6j9lLKB^EVu$wAXKt>L;t~#d1_8GL(5IxqdUrHJ2G| z6Y0ygU)O(A`n&Y`tdD2&`0Ti!9QTuc#0YfkjK2-f`hGTcbu`Mgc#!WHmAl6Cr#wsK zGwEyYUujS5JeS&X?`X@=X-A@G%1`F+$@i#ql+q>pCQnbUQS->eOdG{#W3b;rI@0#G zso&YZ=ZyVD`|HcN#JcL8XF$0JZC~0t)~TGE#&y}cHo6a{_^IE-^Uz8=0)4O@iOj&_ zI;qWVcFkqiW-|6xeW}ymR(}Mdu@8h~3`7zGA&ZqZ^zxD5{n-;)27P$H z_AIX5X3;kX`D2mgSvVQj+<8AUhBBAp#!@-n$}t|~kDLnk-H`)JNlRPyw%rfde(&0t zq;ttT&b@QAcdX5xikgO58}Y8SU+pI5Li<@C&1kN)i}VCW?WeMhHl1=?V)7HMZ90} zp_DhujK%u;x*W7)W@2REu&86t%*4pRvKKFAto7Q-x^KKV_KW+$I`4k*ow)d3ENjop z>vlfB?%3|7Y!@lpgS0rFU+dHPwIRi$`^wszNFp;ZG7+}wi6k-uBVnhGNFp;Z5-N2> z5}ARK%KG%U92~d$cy(`?U+eRhJ;A(p=DjIvevW%>*_U!T7v=bUGqvnlsLe-ZKAI%X zakdQ$`$BBtQ0mwjLz|A)X4e#Sm#Und)+fpCZvtiAOM;_QU>U;HUShM1h2ezd?NZGJv#Ur~;oDFMM zB=eZeU6y9)v|c=mJTJ&3B0Yg=rJjLIBGNNHijzr1dd3HFGKomfaeML`#>sCJ(xN)~ zZG!sbw}g|wLwO!ec z#KrqUJzM+S5j`h*0v%gp*pUX`#qOwF%lVkV!;(0v%JQ4Fj1(q$kiZwc0R{Nkn=A9Vh#1&38jh#j$f- z9E16EK580TsvX`XH4SYs%2QL7RGWsDj=;i!s!~rw9q+m1HtqbPYg0zHw$#LS+B%6qV;p)Ha=OS}=Mp`{~~RGVHJNeqlk%P`6Uo z?ZTd!MyzFKPv!kv)6mkfh#zP=F%ChDJu|KMbyMEiRLt7!SxQ@4ZQ|GWiudi;_lkF| z=SV1dX(w$xvs3T)F=>_#Vq-eFE~WAld(Y#=bNIl~Gx+2^cjn^VIcYm*2JO@@>JJa+J#Yo2RuqIe@;$=9WgZIk*)PoN`vk0l~KfsV;L zYb2A1^aLjFs*y}0(i50nQxhYRp1|VWv}a~wWFV7>^aMH%-d78IW+p}kGKok}pyTL$ zc3`G;txeLAflMOO6X;0hCzrV@*Ji5^4g@+*jTi?4QTwEQXKp*?Q>`5>4RujjYZp83 zTx!2NSM1obVNG}PyEF|o6+51R zreepIb{40mVn;iPQB%>5%G9)jJhkhwB2&BebMpH>r|5Tl4lL~1m3nF7B63wiIGeqGC0pexh}Ntl>;3u4K)=z z&i+PmBHL!?-)&X?omTWaX7pQn?>D#TH?}|iLx1QG-aY=W{|~qC{#SqH_V&lV_x3k_ z{qFXE`tN`Imk+o9{$Ky%?N@&N@7=!dXMgnT_J8@${q?V#_uoI=;t$TZ{G*3k{Gt8r z?gv-5yPvIYcfXQucmH9!&42CgHvin+?eq`tZeRV2yW93>?{4kixw~cM>7U=-e(#Ue zw?Fc8{q3jzcE0`mzdql7;BVdCe*72jZZE%j_w|_OA3xkKKlAqX{%?J8`>tR6{@d}d z4!5_zaJYT(Pp`M{{EwgB=5OEK-v4cV|I*#YwD>pZv$^ zx2pcX#(wbkeSZ4`-+X`j{_ptke{K8!(%{Q?)9rhH_x1L}-}l+=FZ{%J|E4bf_v8PG zFQ(hi{NCg37k=oI+pqof@49{X-tWBqoM~+U88Nj{beWSZ6P6<*rdU?NhdCx#64sbvV)KAc3dDt_VmUI$6pPA;308zP zrdYsoV2vqOvR@`lv7G#}MAn#KQ5ms-6=97jCN>5}tmbio zEMVT|fpe^uVVN<-N*m`1k@n8zmN4D*zBI)+t;%30GZ^t8N^(Tu?^8<%Y23JnLQf~~`a^Q@IzI-ENhF~9A?i1kO#4@cBtF576ph!{*2MIp1JIfo#0>kW3+6+j304R zICe^$pf8zDH2U$#MKmdzlo&xvVlKo5M=WvKG*Kg=C8H$`RIUhII$+XwqF`@h2vraSTVU5;q8E z#3(V+!AM-RzT#hrRrF`Q;wrI)*kYXNl%8xmA5W=)Ke0+|B{pZyh)oeQP8e19$hk+~ z&fP>Fg|(7LV6Acthw+$J*c2XE3OTanNa%n=VVLZG$)$pkPu9IAx9oFY^d6=Wo=P94 ze&5Z*zALcJ_FlR@H<5d?`(Et18e#AEcz@KPMn9l0QxDcyPTfqcKrNw`P)p&`z#R4f zcaT%~_IX45yglZ60OJ8UPF+bS)TbZu#8v7S`m=$CNl)-UcQYDlqPOrD_#^z0+8fmL z)C|-N)-QS@)Xa9@))VUk%mQnGRnhI?O=uXEd-UjmGr^b9Q|UoW2lSj?jQegvOEoRj zpLw?h4HH%~@2(y?Uk-T(FgoJdQ#8f0>o(Cf(kqXyfj$K^^&?}#*E7e@p+zu7gUsZc zco)_)>L>Qp-i#d3%TwMyKJ+ipw}Q^ZS&E0;!wGZxm=CxYdPlz$F6CR8(LSNQ^3FEV zzhU~(IAOI=i(Vyql<84sEoc&YFNayZ~S!yg_`Um`IheW|b%Y7FGc@Hp}h=*ZRs^2BoBdg~GW(a1fb zO{`D6flznc4BrIb7JQq=2ghuT#z&7TK1%wCiaf#ONKd-J63YjTThSF-aGGkM0-N}Xni+!^iPZfdMiEY?QVE`gS{)xls)A< zGK%?e-Ww6O~INv|@s1Z@G0g62feD*6i?1*T~4h8RVgpfRC0 zC?ndl^~d;$jc`?ZHN!l??E0@nwAGcFhh+hgpGjWBu?Aga(@fZBFX~M4%lPyM>6O%0l zO@+8hT(LM}lo;u@SHEpQg%Gx{nyCeuu;5?hHarj6JX_f`|} zb=M{-PKHJ6i8m$&}sFA6W$(OR#+h1)^Yo=D9mQYJL z6KZL&+Kl*UJfOeS4VWC#uR|CyJz7m@m`nrpEA8Tm08K@bkDby6|2tGnhrJhnxGa86jY9{s)d(oR7MmgdROw$qlF!%J@DPZUE!{97r3)=lP|-ZjJeX&HSY=U0@h&6r`-1` z+-I=BX^f1M@l9|VA9G<8&U7+P7&#kwcLQv_al$vk=3(>b4g-wFhi)-8G%6akz=x)Q zHbJAnS@AFU7Wz}{UWv)XWLOh%iMR@LfqAm;%fZ|8t@ej}KX4l}cU#F<$eUqJum)p| zCw#vre7i8itMEo|>~Py$&Up9CqxIp8cW}mg$Mk$PTOZ&I#tyfz)-^ZGa1X`|@5udo zc)jtWSBw*libh4Fz#Qye^D+0&9GwB~3GPZvg}cIC;4a2Zd?J1})&;DN-$FQNI43wg zTmdc*SAa{53nn!tV;eEgoOQPQg)z!G-vetnGmHtw1fv);b7dZru`}uwnE7^L)p>W- zbFTx8&X|oC))CzvK5zGHtmss92D{&SwDNYx@J{$muvPpjY=!=mH6Qo4fD5MCnD_=g zECCjQK?2_-9GQ7yOz^~kwJ`?XDqI<^1ean_uJGVSB;PJ*x|qC z;=bS!IK;yRhmQ7r!()svXS*-<(+FpO!dpi_oE6SOUyA8A)?mEo4VYZ;&7bn_@qxD( zFH9Mx1W$s;!yJeWoCTPLbKzWIR(up6jpQouXX4WLb&G1d|P4HFT z^(fpKTmdFQyTGJ_eb4BHtH4#@%64yXCAcbFWzA)87ai~jJnD^&5vF8p-WZv)j&haX zFnG}C;qY(-IKui)ufRkO7&6)u+7*it(@S)l=0pC?d(3Y!TG1$IoX}X&SkYL~SkPF| zSUmkh=Y-CRPLIZb#(+jaqnJjtDcaO@u}-WL>o}Tj#=yRDa319n&8GJpeOGyh7`nuC zGFRp?nO;V{#%~B3(I-6`(Key2nl3bD$2V_UOowTpo=`)nQP7c12YU$(2@P?u9&(13 zj9F=8W|kd9Tq!=EMrVN{r&47^BAmbJkIAz=t#T)b_CGv2X(n=5qr+Z(4oi zp7-C?4gdK6^>_dB3+>{e+uN64?b^Tk``aI{x4-`Rv+bocKJ1Ozu2|+{$jwC;u4x}R_VE2RIM{o76t+r2kl zw#RRKTYK&U?|GfBn?H4Pd+Wz!xcNrM|90>0cfGm2?`@au z&YQ3QTi^f2;VoBp+k0<4Yxm#rruLaf-~GB2|IGjBjl1pXcOJFp@441~@xu@Rmbn$_ug~P{;~Ft^W*-gs#ot-b-jA8>izESbO^*GO4`tgiHXLTgeK+& zjWi(&0R=-QK@bTB6`DY1=7J&!K_m)JbR+?FkdZiYd?x+_PL7{-9mOPsn28OGea=2- zul4|LhwdVVM2<&209|Z+!aopf~(;O6-Vt5}*9kr{DO? z`&{tPTXr@JVRZ;`b?~x0h%^UxjQU`u+FMy28d>feSsZMr_f4n{&Q*t$8=^ksQth3u z4yCLP)-Mm%Eeop>O-5VLzh;Jt9_T2 z`#x!{)uC=?`&F3lmtl6O-TY9w*`Z2Y$N9k?W45=JYdbr*ZoYT@Z10-+!LhC`^LK`!L&gGC!1K%+SeoJ3HiNzRxvdHz## zK9IXx;Gx%(s^LMhbpgzub48`e=82j}h$=oe{kW&k1G;c24=g z$ty3`dh~ntp+}e3?-q|%zj1mY@CvLAP3AXGGHLp65g&G7);6<2jLZ?080mj(H*yNu%Xn61^s3WQ_197(*29 zg?r*S2FAcVAz?4#dFI(Ko;kgCjE;9036TxshH=ffW*is?#-24jW5<2R*fO?^h55pm zxzFU=k#EPiW!y57kB~^dxZX=7A0e@4Bwr*SA(4Es<#}NwUnCzPQ9Y(ZwK_F5QB{n@ zim2*S)95{;q*u&}v1(3D;JV@3$5%|v{k-bUtnN*c7bcY_#@1)1Y0gY-&x~bk7#*W! zY#i4*&zSQQv#!_1>vQhci?y--%xsox)A&2aGiS){n46Q|hBGs3dJ~wBo?79BiPTHv z8Cbin2-XJ8v09r2YrQ`;hP6(p5zxwd7oVAi>)@XoE2k#1KBXr{+#QqCV^iW|<9R-i zYfT;(&&;lD%t_vuB{}D|Hx27*-mwWU@zgA+VKlcqGa)}UqWY7L&rPuBCWhzad}fL~ zHbOnex=77Tt)4nH`37n=kc<5N@oBa`eS6WzHH zYGn7wWX(_BMBdU~o3>n=T62=WNvEc##+JN0@)q*8@1D%3CN?a><2i5Y4}*_xp}H|qi;zlF!S+2Ui3kAk)hUSe~{IJ9N_cwrt- z?SL0*RLn2s!~{GF9wT#}(Jcm8p{pL)8xJ|F&=Gd*JNki+jAn;Vk3K<5gI7UIjbA}W z#aWi{9Wp-f8{IA6)eU=b!*_MV9y;f{x-l&p3;eox-z0R#`i|-4zRA^<@r54LjQDY@ zmyE4mGE4NW(7dAN0^Jq7R&a~lCuOF*WCueuqCdecM$TclW%QRszu_0rSn$xpFok=j zSB9DWjNSp3TR3f$Bm6SFf@*i?>yOW1zfo==qrFtMD8ZvVMFBvZd9}e!mJYZj6mcsBPeS)I~a@VgKwZ_NEfDqZy)Z{54^*<^E|T~Ms&lwG%$yEr{@y-1Kg!z z4>87ybEl>k)Tm+a$mu)u<7I(%gYKp3S1*iF-*cCE*m1sG;EUf;d_?+^v?uhV#Y;<1 zI(pG*Pnr#Vq?c&z>BXRZL0+yrf2WcyB<#+XGdr8hNp&JCOp;aoeA_VuJEV5!ISUtgO3G1{F2`3c}X8jM6ddx zpE=#-Y~fpp7ck)4K(o{D1&=<{KlVwoXoGc+H$i(v4=p`x=wC#ypwZ*4F5HJJzMm^o z^Ij;tOZarjJZA=e13mTh)Zw|%)0SR3$8Q-qES?+neB?-v`7KEFbHH;;k9+k@Z;QP$ z37br6*@Ss7`p$y8#vh{@1YR`8G4&6W)c03mD*E=&$3q zps;5P-Wz(I@m|yCg!f4Pie5YV+(v%CIAgr_8t*L}3LJ8|OTS&)zTENBpN?r^(-TN z8c)E-$|c_sdy%~o=}FRlP_u=5hn5!J7Ec|V8_!v$f6KsWP)4vxun{7fGt<+P4&Ih; zsZV@k@NMCgVUu7JnJpY{=dB(dDG^ zH?Gt-d6rt6dXf7`y>!%q4r(UV<5klM*Q}?$P+wRl)EDXt^@aLEeetSsM_iy&HL5-| zQ!mH``WXFg%xFv68?+U)Wmv_Aeyg^k`nKZ|Ju78Ijf(jt93uDWsPhz^su5l2jMUC( zO=wMMjp&Hf%16x?XjZ+d6^&>SvTY9@47ie25Q#7xrxuWI*or$@~edIpzEbAQFBU-X@Mt?+GL|0bU zs^6&(G~gk_J;5}hSEwuK74!;v1)UOJ8UtDaT0+E=yu|m955MKR#EXTy+m0=M3Vu5L zwD`$*Nq7+=R-7T^cI0v7@tyipZ<}#}{sp?1+6VPOL^NdP3SKf^3O+JE5*{MjBH9XG zGF}p15?&&jTKp8vBCaPHv4YH#r2$mzRN&NKCLI+~*wbgLJ9MBXb$FY1H6L@zQv5*{KNBO2SI zmmOYqV~2-UJ&;$>Drgn7iiI^EB;03D;UUon^%3#G^C0&*tJy2OXSFwegRjjPn(f%* zYrs>Fryfro-)hJCp7E6Ml<*|#ZK)pBpL)Yrj~{$2v>!ymOXOLJCu4i;3wtW5C;Z?g z;U%IuqNmW4jGv5_L=O_$db|nR1?_@vL32c}pwVl8@D>U2M7UP`H*AMtp zzj)1=y~VRs_+EG(TYTfaq?55z|M)KIKNATCcu#1Ic#LR{Xe}@(Fvu{-x)1Rrco94Z z+9SFJy$=806WU+=2mD9I$S6*kFop-537jJ^apbquaz`H5>_6oJGkL)8I6TPNz@t_k zmGV#)=i~awLOI-l0}P0`fPwB|{5$opdlv5*?-}ojX9@4I!?*Sx|7~Qia>ler1{ciT zV+|aN@_>ahKx?c1@m}zr8yIN6Tiw_AL2E>(pwHsp;osrkJ7vMBdjRhN?*Z>gd2k=O zPs)cmv9NK<<{@VbYzj=^W8u@l$Fe6aj6C&1e8Ari%IN`|9`ZL3--spLdaQNt_%*CH z;~Gu_=U0YJf{hT-zz9ATKDIv@-C9|};mAoD!H4-y_Xcd*v>SWn!@g+w<_k0| zVbQ>%!Lx;hg@s%5O|VZgEYgt$3}7)R2lWq!9tIr@S{Sr=?T)^;d@FcGL()BgXF*GY zU+eLVS9kP^=Y~+v+-KbfTqpb{{Kf|F_L|>-YtC5W==}!ow_}5M%bv9OuhnnDZ@ghI z+~E6mtngjqyT*5O^o?iy3K5pft5yuu*kJ>fm! zJ=XX6{a_yd1^)r>;)(ERtYOf=pn*Zd{}vE&7h3n)5B*_ zR?4X8InBO+QwygIrv#@2ruFiIqn%es!o{IFsu1{rg5-?nS)s%d<8q@ z2CG*04y-y@waSX~!NV%RD!@woBfcBP@OZ4?R>Q51Q!gPRX56@nC!m@*9%Q>52ne{t^Wm1*}rs$98kLVRV2p$9v zf_728Fs)!(!L){H?b$c*bnpy>U%?foS$Qhg>OQ~q*Ze-f7OoDi9|@;XH$}L$gO`h>tnPn1i!}vqyK3C*u;< zEB&5fUDxnd)-d)k79tqt;oK-^Wenc}-wfXkeAl{Xk9@b|_Q>`wbIO*0X$RAyOyQZ~ znc$g}Cz_*jM5~}x&?o3w!Epu03XYX>gr8G>u*=E~W*y8d%pA-d%sk9ItUQbYyaK!e ztRmNuYq83a(Ldq5SC&s;`4pBHu!LjJzaOE)p~vT2ejAk~96NqnU}$xp%F}TPrxl#4 zBPY&3*a#8kYuNDH1e3f`E_}Zk9{L@#zXn(gu;^jYt>B?7;E>^vT6ic6|@m=R5bx0k0CCjTK&NyjtBW_-ycL z@mb?B#HVR*zk9+tt@?T2KjmzGjDGFcQ}jQhpU?2{ob#FAV04Fs z-uR4t%rkjDF3`L{^AgQVG_PuOquI~aV~6e*-DSHQcWB?Dy;r?xEy`b+qgBW$SoJ=HLtX#dc#M#F%Hf`*(3?HM`}IwCqEc_aCR+(K?4ZzQLX)2nal z2kJ*cJP}?&3-vRR(4ah66XMJ0K|AY$yfV&}4SE)6*`Q&KhBbdf4V*t&>r*>v?Scks zM{-8;2ziCPLcT~YA(y9ipmv~kQY~CZuEi7KRrIJ1)xf*FALP|>M*TVUjn>!N)Lqxq z*1B4g{KET*8j&@^8c)tZ&Opw@b>up7E&f$<^IY>PV{%5;3Ui6Ik$I7Ku;x|P@E-Fa zam^@<>G=FL{{4Y}cjx;Q#-!H+*W4!}=p5%&TrG5CBrsfN?Nq&De zb=aW>69PW>-8)9mdy&HkqUQI`JS z%)fSiGQDwoHNE-T)9Fj^f7cJ_dj4zArzgMq_!vL%kcl_H-A})A=%$bVS^L8t|Jb?L z-*Gej%+ssseb29d?7RP$7aqN|p5FcP-RVQ`eK`HbC*Ju3DgL+rPu#Ajw_ZJ+zW#yx z(|11eY`T8Wi|LgQzBc{BXMTQq>&qXVzW&YM{JtD-eyyH<{q5EC#c$TrpZ2wIhPDj!IBnnP+Bms4hkweEd@gHzfe%8-X+X*3N;(^89wf9=z z$J)E<+{1n4SHF7uy{f9d_?eepW~jb!QdPhH(yK2dec)%8SP>s5zWTLSUphKq!q0bY zRmBhoAz~0lVN8sPF*D{(427}q+%Xcq-$dUtc0(k@vqy-}jMR8)ecFfzix3aCMxcg> ze$LvQXGS9W1`iTY)oAvtf~{&B9fixt|T)Hmae^%yTuAdbTNsvCIha(XeQ% zO^G~H@0FLeCC{y`h7iyYszJl27lC( z(`7>+(c^dM&kjBfKGh@Z_?frVp)CgXi4v(}&C_eoxYTFW8w`HoGX}qKa0V6~Jvaux zwl$oe!K1++HT2Vp!JipIQxnZ5v@JM(@X--=aIwXEvuULsVrz$J9X;@9b%Y&)Bi}Uy z`<8V_ZK*jEyXmOyr6Iu1V5gR#M%tF{ZzBp4Boawf@sf_?1=eF@qhY6E(MZSc9q=bE|p%7W1h zju3h*^eKW7!47cSP;acQ2cegO525GMw1d>-MEf*183pxx6)+teEgb`$86r%xV@jy^g1uZMO(KNInYqT|z~uRXNrVu&XHJNY-GOm%S;E0z!`JFFa|5=sXac+C+3Gwk zOwGD)ZyCe7UoMTew~TYQOv`8)17kx@WQ5#!ciRZ_sC&%4zikrF0*0_wgA>3BtUJOl zmL{>@uwGL??{-XNUs$)A6UYgtCVB8!^Yf)?%hL4Z%?r7#32T8IORkHzP2kx_p2=(T z(llvlR(8kixE<3~yQZVI*IKRX$>m%ud4ZZ%bIB{@Ik|1R*{<<&YC7`!Obz)$z9Zj~ z+se``!;V=}v+~rDAHnI&u1WEcDb#h;1u*)w!>^pRv9q;l1Ae$4Hr_Hmof-$01uI2* zA6KU7_RRv^1-MIaGq?rJt_C+eG9nL60k>03@T^&z$XWz5`18?$6%-e&7ObZ1a~=;k zj|ZlOvw`0V-1*cj6d&xe#XjP0@eFnW+k@@Ew)L9+P#+$cfF}!P3+9~KdBC%rnYlw7 zI2oL-S(_sFxSL>s6VVdjDgq{W4H{bIFRXd)BluBs{V{or;fP)w(R+FgmxW8jsZE+^hR0yZhXY3)7>ahd(Cj;4jJ_u1$&eOiK4mRxOGP|D5+^kq(WA)B6Kchdoof zJrh0QE{)L+}U}0Z-W-8;7op&Ls2=ZjnRo(mu~;JfCs5(HGD0 zcx`&GXLn(;YHQbcyfy+?Mt^dwqgCKfTdjBLDeo;jDbde})^NZbJT!B(&)Xxju;_4{ zD;Ta(?Rz|5@-AMQnzi7s&jF9LT`FZf9KO7_BZfpxSb^#ykl&IKR6>Zea3?wekH zxFa)HFEyIO72l>kaEj;1GJoT2IuJJo~Rc+6U*7aQ?}aigs%*L(eqJHp8Qtt+d1E~ zOTNcvyyIZvNATx4vjBDwy%(IBdStK1pF`6euaUdz@fq~Y(C+bT@!INH;wiu%SqtjX zU4e0j-w~JegBrdGEjSS_3wDRs4v)Qh<@`L}oO;7=#%mGo9(ofRJH3bK?(k7OUe&Yu zL{GwZ);YX_hJw!y&xPDfy};j5>%n&F39J~=fR;oL9DX{u0$MX%Q8C~ba$3A4?FGI% zGzK^vzPk1p-&ef1+<~$)F1nqu?{~&Iu^^T@pED;?duvS1txN;!s%-k|}_V(zvYqwcrPB=#nqh$=+wbL;&275YscRC95sC&%4KOGa#0*0^_z3!ck zfptfCe>Nu8Mc0Gw(*xE6>sE6DIe{GZg?(XPSdW1`_M$%_&c;BF<=I88IU9YXhWtdX zM6IQceIZ}Sk5TKXB{%wG;(QF$gm6B#y=96-qk6H1-PKqna!3dUsDd36ctig%`Pdo+- zt^hu`5!}Fj^oNY(_{%YX8^8^by>K};$_r-;XS3ypJD-gO-b`OJyh*vh7w`pK0bjrt za0SdL@Uj=<5$B>B;0%YXtABXm@#qNTM@RSr;%bbl1+I|J#|8}z8d`zBzz=tUJ3H0E zi0=$`f+wjKI0Zg|L*Nkj1Reojz)$EBI0AVw5rQ`D4>d6!q7yx;F`xzhDCmrM30G*< zezERU=RPA^{hcwYR3VaD(fit5&IRXy?zrY{BiGp@K1n^=!zz15@LqKP^Gdk4{PEg%eJ?!(0eoHto z)Q7)2MtlVIfc}pD7Ib&&1MLOv*{TnEt@^X}`haKdJNgs)Gy0S2k5}kNGyEAJNqwM0 z;1;-(dcjA=N3!aPeY6N#1RWkvArV_&Xon}peco5~rJnW}(H!oM7H^LJc=e~=TEgNF zyZ~No)#n|~PkrLi-y7rBBcAZn;jP14+1?#=;U()?Wc9@;mw3k;cRO#r-NDa3e)c$D zJ)^s5#}mFXzKZ(7Q^r$j@ut4eAZQS{o!&XTdAx;0$N}FDJwNrh$F~VT^e%uO!yZ3clBKbmTNw^h5pL#V=m{_1NnS>KP zwBaY zl+}x#Wp|G6i?LHreDefOfwOGyr~10)%&3PiS$d*p=@|VUeZ!mj^7luFC!Y`b7OSr{ z+Sh!a)z>cP$Gyc*06Tykz>I>g$QbUA4c=PrsK=MrGi&z8IbP;?>D7xp9_RS!@zZlh zGG3B;furCfs|WOVTmARYe~o_CexKf>SI-0O3Eb4G2ehLc*%H4@Cuv)rhu8i7jQd#Bzy??8SSY-`&R#&cj`VKZn(!cd>;t^V6^yfMc*Cm zyZT@ZJG@(}AKl;w)tvQSJwP)$(dyB;&_1`SQS}{h*Hzn^cU^Vu@?GP8Q!8?xGf<7_ zis(XLLZ5(F(3sJf>bx3S_?WeIy3b`ZPp$p0<8tD8LbJe0$xFHMsEUNz%J;` zV5NFJ&e7aEg7-12W_!c=qXEqk&Eg3EXl&8lqT8a|qT6+(GkVD{=*?(NLO-`UAEFN~>O1rmYBRMNjmfOX1v=;GoTGEj{S0V} zXp3kQNBGBRRDEbuU2w!s@4`dwE&*CsPHt1;4VbS3tk@P;U(6fV{N9ZBIkD{6w!;{g% zn3k0qg*76lx-4csg46UBx}77abZ3Jrwp6xk64NU&!mwQP8nM#}XaO?KcAqcw2ZZ zJTAipj}Vc+U1HBO7=ijIcNok%-B)8L+#4$jt%uK^|pYa_0$FGgoumPIH$g+z9z3BuifUMmpvik zk{sP4X zKmSkkiNE{(>Fi6-O@H*`YWitE|MhQI(|`W^H>UT0{3p|=-+FnQ|9fBm!)<#1{O&Y= zVLdHhKA7e&?oHJT?Nq%Lr|SL1RQ>brwElx?TEAUQM}J;TH$SK*|5i2SKdz=F&%1w9 zP0xH$rq8|IPp^JwIeqW%kEhRmyPCfIZZ*B}LABMi{_??e^5)re@$KvB*&jYX9e#5) zoxO7~UH{ds>B*1oOzYRG>Eb(JzgJD4`lo8Dz5@TZ{8YVLrs{83Kh4troAqZ;4yKo` zFQ(U?eK>vdv!DEkuE)RiczW=gH{1BgCJV29u$q2jSWaL0%f(OH{+VkpeEi<@OAjxm z=O17F%-sJ=!u|8z=~GW{O`ret-RXB;`NT&c{m4tLL6f zAN$<1(-&Ui^|Gey;SNnEc!uUzte*fCGZBxLdYPoffcd@EWBdLx;Fd+Y?5C#t6^+e#v5At zDbD%6FJ9+FWkg2a{^no$>CgT4YPI_DANu+?ELQ*WE34IC`TEa(Z8&%Q_c@;PWq$f+ ze)j8s_xm;ccN4`aA`Z0+)K2p5mTxPI)%XAk|jdTi6h<20=wV%$7V^Rvex z%Ej}3HxKi?dZ^|4u@$*;^*EPH%gcwkTs$mg{g`&EhtxfLNWFINxQ5HeWWUL}DLBTw zdQAE1A>@tr)#GUWke}(RPdmBYW3-H)Ew`52$9$Hw#d>csCDyyn5`6Pfr&w9(Hm4u#L|iuA^~~8_qfB-5bkpeD=6=-n+7T?DDh6KCg|R zW4DiF-@(2Uqgb?VFwP-4{^a#givxjk68TaeQXsn_!i^eWE&)_`6uznn!=jc2`HkRgKGzWt*?CZrt zSDj0H_ONUA;TWT1jE*rl*Wh{y=5aKSqcJ!**IKR~+Ijw}aj2_@zB<0*+>80Qt+_pW zT-xekiK~Y>J$sy7OU=1fvi%mvQ1a%Xn#bxotj4FFYt%N^TXCMHY#vILua^(C$v62) zep)?d=UJR%8Lzl}EY7jIZfgJRp?31t&#u?(ddHD5i{q!l4 zise}jYJ(aqbC&zes6Set&z6JbpbpEZCQ#F-PlZe(5?F83x=tTHU0PnT+NJd~)?9GO z6&tQub@Hsa;F2pgT(kB8bHOE7gjHULXL&uW^Liu@87}k*L=wZL7=cJSx39EG^^G!nPBqqed|{{&=t1vZJHtYRKyi*KFC*(R1aGcf&PXc69XT zz7M7y9XWh)o#V&r6h2-@#{AKGU`!v`|EEtkm&Gv{a|W>xKUyd4c#Z}0z|B=z-;(nu z=JONt{E4~x#9V!1uEdB0ZZ>MEQJzd963+dgPvquWEjfS5cE9v=vn}hJU0Fx%n!aTF zU$T8UB7qvk$s{7-+z!nqx^N$umONUk5-mo|Z1^oay=)+Z7e8OTYT zNMK|jMKJ=oePll$+0RE$wSM$e$Rr|xTExjDB7vGedb&wF(*KG2TAi4D`28 z_2TBKkf|3(?W6Wl-TZW0XUAh%EV<2=XK8-ApQfkh+;QKux#N}_?yDGg+;YQx5#x?q zZn)24+;Pjzxt+AR;eKlN)9k0&k2ZJQa>IQUTj(+-CkRAyRqc<%JS^8KE;dr6i63EyJ(lEk>T85>oYQ(+s{~)r>j0cU2?&i>n=ZS zxZ;uv);wdy^-i8EF1cXMGge$}#kpY3&2Y26=^1Ia>znCzT`vynLT0#hOavl{qtCa& zXV*x$5+`ZzIdEj~c{LJYb1R{xrJqL~quEUW)B(bpP zz>(mZjihZ|&n)aYa3t(}u8_z~%q;9Va3px>j3hFRiJ65x2X=im2h|+dcW)gMna0G- z!kz=$p_(7-(K9d-2J4e3R2maA3)|7Ubo302gweVr3YEsh%)&NUmyVu+kr31}v5WHb z46&|90)5hFB=|g!shaodJC1hN=BR!2xqb8*y=ttLfvI)j&w-=+|I+k1aHQ?0^+Y>Z zQjU(D#>7my5u-6Nvz+_4`phin_MJAZo6X^DzGm|^o2&V$(wLYDgFJ~$q0*R`38NYk znL?#8F%yCs5}87!F%hCXiA;&J?*Q|~7T2GcZza)X|ujS=imGrDtHI z-Kt?`p}Ui3x)UQ0i|b=?eJrj6Z7M0~Q>ZjTT+Fu|nL?#8F>{%g^%WthC6Osq8WS@M z7kODa&g0+B?fP-#rrx-482>vF}0Ya)qEq0*R&^;o#5*5ir|*F+MTLZvZP>#=az ztV1M`DO6f>Ud;5kw7Go*=05K42%RKiA-1I z85jvf5}B^bGcXc}B=Wi4w9hWRYot(Vq$@EBl}6f#QK&T1wHSp;W4FCAFLK+Pb+#;2 zn*MetPe)JMsi9D5Y&$h{^bCyTP92rTo&!gAy?S~EMgoiXaC;6M=>|0nj07UdJG+HF z2aa^3S_Vb}k>tJJ!kz<1x^UxjM=c|PND|+3;7Av3lYx;yB$3%A=RwcFNFb8P?6Mkq z21WvrM5Zh942%RKiA-1J85jvf68YS2+T^QS*MN7@q|L4Q*Qe4**J2bZjkFb`P-&!{ z7==nBb+_JYiBV}RVtdO2M|$tW2Sx(1zx8g*dK4-XGmHG514laV(|ZO+0+B=>tWTve zF|)|+IdG)&UcP5wBoImD(Ke_wCT147JqM0-!8RBe2}BY(*anrx#LU88{=ktwTAz_X zB#|l6`ZOkH7WNz%lJy8g5}A^$M`L2h>Ig&<`P?u1B=*HNITETkiG3C4$l{&%o&!e~ z@4xpPIG)?HHb+u-=kNN?yx)1Zb7$V~%<-M~1mYAb6Eh2Y4jhStS~7)7V`64u&w(Rx zR8OW*X-v#4>^X2G1nZE<6e^90nT0(Ejtue6JXx1WB2%a|CT14)92k=I2t*Q@LZvYg zvOI~L?|jF+^UmSUJ0R_H=X;|*3;SZ*9BEaKnT36o<49|A%q)J7+H>GYo8*{T{9d)^ zz>#?tV?T>=BrkW~Kiql$aJNqTJKx>(sWj3-j6$W6j$#xl?cBf7r;>)}=J>g}eQs`_ zn?rpX6ETR9DO4I0Gcn4ODe<|#m*@UoK=jrVik=k<#|GYfkT zv?|Wb!kz=Ii8HgX=fIKO^nBgXCN<0~>^X2`J3n9V=;$d_=21cTXw{FZw}=tR5nQsTVj$YQ>Zj1VwNLQ^1XL6_x^6=nCXl4Nn{F@ zR@5-lS2ZLug-WYxnAx@abw^JmkttMKQ_IYDa$eK@Q%A2oFcOF)GKET0x1H^Sj-Fw* z4v9<=t85o_bo4|LnL=f^mnV|Q6e_!e9Fat(P>Dw|GKG5X-{`X$KIiwp&-wlDbAIR3 zX3JI3=b9}$uA&&%Y}t{>Y?3%zB8g0)60;bYLZvU_Br=6cU&Tpe3YA^^+*3zSB#|jp zwv!qJ^bCvyVpdD0Q0dDXo-Nf7 zi28|4q0(11RSkitpU4y{yXKhb8Jb!GQ9qF>RJN0PI(i010+B?fP}$Dv>F604No015 z96iJ4h2Pd+_-*}#-_W%Q^jm#K0+D_v#z-L2cVdhLB7HB$NFcHuUYPF}o=snPE|nv& z9mVMwMsWh0@WR}`F!%Cw^bA1_flXAymK_~ELsUZ`lEk)24IMp0QbQo>Co+Y~E~}+y zU?dPpZ~9&SO~1>(>GyYS0{vE> zkwBzu-}LV3&2^*S$ukm&~Dvd507Y0TGkwlL2NuGg`KqQgr zvm7IVNFtxxMVnYHDchI6-@Wwx?xk^k>ARgijfvceQE5zs{-tqyY24%~R2mb*@Y1-c zBa+AzDvgO@R7W6^$P_A#i6N*X5J_YTmBtia`p)&zcdnP-3#um&Nn}cV>6!7RXU3Pl zW2t9h&w-HClW0i|GYfkTg#6NHsTwBjnT6cHWgOlz4sSW%x16s&l|~xHC{!9Th?6N) z8WS@MA*v&hDO4I0GYfkTgruHCPH*{lpl|tp_?GX7>X}*Cb0B2(Byv_yr7?-kEazO2 zqtckjRg6kwA~!KAjft5&iBoAz%#>M-#zYQp8;iG%#oNZ>ZRh^BbAQ{pi&JS#gd|TQ zQ>Zj1VwNLQsI+sx=u>G-9xdNFZjlWD1qWuBfMHU?h}xeeS6zQ>Zj{RXsfeBcZA%kttLfT~o`z zNN8$FWD1o=nB+)g3Y9R6lgQ`xqD?=%a?g3?dE%AliC2CfdF8heF-8KBE{HQQ5{M+a zD96A^Ad=!M5}CfpF%pO*`YO&y zAd(oG7=hTm=UwZ2?!E81_iB?V{d=BS-gCW(lgN}ooJJVsNMy<=M`H-@83WrTlE{>x zhQ`DY)e(p!G9{{`F)<`{1S09&&e|kO|GsW{lGQ;dB^&B$NG84r%fdV zeF~LEjN)Vpl}1eBWD1qWM9iP}?_lI8R2mbp$dM^jS`n|}WD1p5?W2iNsI+r`(x;Nc zhmQ9{$NQmU{m^IChmKW@N+YBX-5)-5ymAyOjgaNC9GOC;5sF-qBU7j}LY1p>WD1o= zY>uOeQ>e6a-~Ss_xbXi5{pBC|^FOq@{6GKy;rIQ!zjJ!=6F+eJ2YOumAS; z>3{zBUp#&15C8G$2Y>k+pVj~Epa1)xm8Y-Hr~I|$RKIaNHUARI&FV>bsGQS>QwdTf4w?=>BsxikN!$J{p@el({KL! zyVDQ<`s(zP-(H=5;rCXb9n#t9r{{B}_H^16WFMjKIdi}5NPaprm zJE!GatJBNhuzAi*efs&+ zum13tzvs9<`s*K^KKN_zf989V0^j<*cKWN2_4HH!qWn?mKQZ>TPhOq=+~;4OzWUMI ze`4+bOTl|@&Zn<@;r{evKltwHZ+`RpzGoBv_xXSG^?drJFWsDe>qp)?{r=B=czXK> zK0bZ%M?XFN#h?2Nr(gQDpE&*2@BFRr+Qzqj^ZN8RzI}E2mEXBO{qz4YoPPHY#_!7i zSv$Y?U%zwu?f>{sPQUQ&pFh3)`0!`l+@EUw|MEZnRONqU@elmM&z!#ebH8}{!aw@4 P)7$^_#dp=5KLP$XKXh4` diff --git a/assets/world/tree/snow_pine/8.vox b/assets/world/tree/snow_pine/8.vox deleted file mode 100644 index b2144f2528bd820a2bd27a0337f6270002359b7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9820 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 = 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"); @@ -70,14 +69,17 @@ pub fn apply_trees_to(canvas: &mut Canvas) { } } else { match col.forest_kind { - ForestKind::Palm => &PALMS, - ForestKind::Savannah => &ACACIAS, ForestKind::Oak if QUIRKY_RAND.get(seed) % 16 == 7 => &OAK_STUMPS, ForestKind::Oak if QUIRKY_RAND.get(seed) % 19 == 7 => &FRUIT_TREES, - ForestKind::Oak if QUIRKY_RAND.get(seed) % 14 == 7 => &BIRCHES, + ForestKind::Oak | ForestKind::Pine + if QUIRKY_RAND.get(seed) % 14 == 7 => + { + &BIRCHES + }, + ForestKind::Palm => &PALMS, + ForestKind::Savannah => &ACACIAS, ForestKind::Oak => &OAKS, ForestKind::Pine => &PINES, - // ForestKind::SnowPine => &SNOW_PINES, ForestKind::Mangrove => &MANGROVE_TREES, } }; From 6703575641d9613944c3fe88546d9724da91be17 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 9 Nov 2020 22:59:41 +0000 Subject: [PATCH 10/10] Added birch forest --- world/src/all.rs | 2 +- world/src/layer/tree.rs | 6 +----- world/src/sim/mod.rs | 10 ++++++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/world/src/all.rs b/world/src/all.rs index 3020c4711f..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/layer/tree.rs b/world/src/layer/tree.rs index 1ff70b4815..9228646ba2 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -71,15 +71,11 @@ pub fn apply_trees_to(canvas: &mut Canvas) { 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::Oak | ForestKind::Pine - if QUIRKY_RAND.get(seed) % 14 == 7 => - { - &BIRCHES - }, ForestKind::Palm => &PALMS, ForestKind::Savannah => &ACACIAS, ForestKind::Oak => &OAKS, ForestKind::Pine => &PINES, + ForestKind::Birch => &BIRCHES, ForestKind::Mangrove => &MANGROVE_TREES, } }; diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 8a9383e7c2..e099bde409 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -2213,7 +2213,7 @@ impl SimChunk { ( ForestKind::Palm, (CONFIG.desert_hum, 1.5), - (CONFIG.tropical_temp, 0.5), + (CONFIG.tropical_temp, 1.5), (1.0, 2.0), ), ( @@ -2236,10 +2236,16 @@ impl SimChunk { ), ( ForestKind::Pine, - (CONFIG.desert_hum, 2.0), + (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