Merge branch 'zesterer/worldgen-canvas' into 'master'

*The* world generation cleanup (part 1)

See merge request veloren/veloren!1490
This commit is contained in:
Joshua Barretto 2020-11-10 00:01:11 +00:00
commit b4d337c746
29 changed files with 759 additions and 1005 deletions

View File

@ -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

BIN
assets/voxygen/element/map/dungeon.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/map/town.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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,

View File

@ -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);

View File

@ -1,36 +0,0 @@
#![enable(unwrap_newtypes)]
[
(
specifier: "world.tree.snow_pine.1",
center: (15, 15, 14)
),
(
specifier: "world.tree.snow_pine.2",
center: (15, 15, 14)
),
(
specifier: "world.tree.snow_pine.3",
center: (17, 15, 12)
),
(
specifier: "world.tree.snow_pine.4",
center: (10, 8, 12)
),
(
specifier: "world.tree.snow_pine.5",
center: (12, 12, 12)
),
(
specifier: "world.tree.snow_pine.6",
center: (11, 10, 12)
),
(
specifier: "world.tree.snow_pine.7",
center: (16, 15, 12)
),
(
specifier: "world.tree.snow_pine.8",
center: (12, 10, 12)
),
]

BIN
assets/world/tree/snow_pine/1.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/world/tree/snow_pine/2.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/world/tree/snow_pine/3.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/world/tree/snow_pine/4.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/world/tree/snow_pine/5.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/world/tree/snow_pine/6.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/world/tree/snow_pine/7.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/world/tree/snow_pine/8.vox (Stored with Git LFS)

Binary file not shown.

View File

@ -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,

View File

@ -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)

View File

@ -4,6 +4,6 @@ pub enum ForestKind {
Savannah,
Oak,
Pine,
SnowPine,
Birch,
Mangrove,
}

View File

@ -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<Abs<Fun, Type>, Arg>, where Fun : (Context, Arg) → (S, Type).
@ -26,17 +20,11 @@ pub struct Colors {
}
pub struct BlockGen<'a> {
pub column_cache: SmallCache<Option<ColumnSample<'a>>>,
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<Option<ColumnSample<'a>>>,
wpos: Vec2<f32>,
close_cliffs: &[(Vec2<i32>, 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<i32>, index: IndexRef<'a>) -> Option<ZCache<'a>> {
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<i32>,
z_cache: Option<&ZCache>,
only_structures: bool,
index: IndexRef<'a>,
) -> Option<Block> {
let BlockGen {
column_cache,
column_gen,
} = self;
pub fn get_with_z_cache(&mut self, wpos: Vec3<i32>, z_cache: Option<&ZCache>) -> Option<Block> {
let BlockGen { column_gen } = self;
let world = column_gen.sim;
let z_cache = z_cache?;
@ -180,300 +66,131 @@ impl<'a> BlockGen<'a> {
// marble,
// marble_small,
rock,
//cliffs,
cliff_hill,
close_cliffs,
// temp,
// humidity,
stone_col,
..
} = sample;
let structures = &z_cache.structures;
let wposf = wpos.map(|e| e as f64);
let (block, _height) = if !only_structures {
let (_definitely_underground, height, _on_cliff, basement_height, water_height) =
if (wposf.z as f32) < alt - 64.0 * chaos {
// Shortcut warping
(true, alt, false, basement, water_level)
} else {
// Apply warping
let warp = world
.gen_ctx
.warp_nz
.get(wposf.div(24.0))
.mul((chaos - 0.1).max(0.0).min(1.0).powf(2.0))
.mul(16.0);
let warp = Lerp::lerp(0.0, warp, warp_factor);
let (_definitely_underground, height, basement_height, water_height) =
if (wposf.z as f32) < alt - 64.0 * chaos {
// Shortcut warping
(true, alt, basement, water_level)
} else {
// Apply warping
let warp = world
.gen_ctx
.warp_nz
.get(wposf.div(24.0))
.mul((chaos - 0.1).max(0.0).min(1.0).powf(2.0))
.mul(16.0);
let warp = Lerp::lerp(0.0, warp, warp_factor);
let surface_height = alt + warp;
let height = alt + warp;
let (height, on_cliff) = if (wposf.z as f32) < alt + warp - 10.0 {
// Shortcut cliffs
(surface_height, false)
(
false,
height,
basement + height - alt,
(if water_level <= alt {
water_level + warp
} else {
let turb = Vec2::new(
world.gen_ctx.fast_turb_x_nz.get(wposf.div(25.0)) as f32,
world.gen_ctx.fast_turb_y_nz.get(wposf.div(25.0)) as f32,
) * 8.0;
let wpos_turb = Vec2::from(wpos).map(|e: i32| e as f32) + turb;
let cliff_height = Self::get_cliff_height(
column_gen,
column_cache,
wpos_turb,
&close_cliffs,
cliff_hill,
0.0,
index,
);
(
surface_height.max(cliff_height),
cliff_height > surface_height + 16.0,
)
};
(
false,
height,
on_cliff,
basement + height - alt,
(if water_level <= alt {
water_level + warp
} else {
water_level
}),
)
};
// Sample blocks
let water = Block::new(BlockKind::Water, Rgb::zero());
let grass_depth = (1.5 + 2.0 * chaos).min(height - basement_height);
let block = if (wposf.z as f32) < height - grass_depth {
let stone_factor = (height - grass_depth - wposf.z as f32) * 0.15;
let col = Lerp::lerp(
sub_surface_color,
stone_col.map(|e| e as f32 / 255.0),
stone_factor,
water_level
}),
)
.map(|e| (e * 255.0) as u8);
};
if stone_factor >= 0.5 {
Some(Block::new(BlockKind::Rock, col))
// Sample blocks
let water = Block::new(BlockKind::Water, Rgb::zero());
let grass_depth = (1.5 + 2.0 * chaos).min(height - basement_height);
if (wposf.z as f32) < height - grass_depth {
let stone_factor = (height - grass_depth - wposf.z as f32) * 0.15;
let col = Lerp::lerp(
sub_surface_color,
stone_col.map(|e| e as f32 / 255.0),
stone_factor,
)
.map(|e| (e * 255.0) as u8);
if stone_factor >= 0.5 {
Some(Block::new(BlockKind::Rock, col))
} else {
Some(Block::new(BlockKind::Earth, col))
}
} else if (wposf.z as f32) < height {
let grass_factor = (wposf.z as f32 - (height - grass_depth))
.div(grass_depth)
.powf(0.5);
let col = Lerp::lerp(sub_surface_color, surface_color, grass_factor);
// Surface
Some(Block::new(
if grass_factor > 0.7 {
BlockKind::Grass
} else {
Some(Block::new(BlockKind::Earth, col))
}
} else if (wposf.z as f32) < height {
let grass_factor = (wposf.z as f32 - (height - grass_depth))
.div(grass_depth)
.powf(0.5);
let col = Lerp::lerp(sub_surface_color, surface_color, grass_factor);
// Surface
BlockKind::Earth
},
col.map(|e| (e * 255.0) as u8),
))
} else {
None
}
.or_else(|| {
// Rocks
if (height + 2.5 - wposf.z as f32).div(7.5).abs().powf(2.0) < rock {
#[allow(clippy::identity_op)]
let field0 = RandomField::new(world.seed + 0);
let field1 = RandomField::new(world.seed + 1);
let field2 = RandomField::new(world.seed + 2);
Some(Block::new(
if grass_factor > 0.7 {
BlockKind::Grass
} else {
BlockKind::Earth
},
col.map(|e| (e * 255.0) as u8),
BlockKind::WeakRock,
stone_col.map2(
Rgb::new(
field0.get(wpos) as u8 % 16,
field1.get(wpos) as u8 % 16,
field2.get(wpos) as u8 % 16,
),
|stone, x| stone.saturating_sub(x),
),
))
} else {
None
}
.or_else(|| {
// Rocks
if (height + 2.5 - wposf.z as f32).div(7.5).abs().powf(2.0) < rock {
#[allow(clippy::identity_op)]
let field0 = RandomField::new(world.seed + 0);
let field1 = RandomField::new(world.seed + 1);
let field2 = RandomField::new(world.seed + 2);
Some(Block::new(
BlockKind::WeakRock,
stone_col.map2(
Rgb::new(
field0.get(wpos) as u8 % 16,
field1.get(wpos) as u8 % 16,
field2.get(wpos) as u8 % 16,
),
|stone, x| stone.saturating_sub(x),
),
))
} else {
None
}
})
.or_else(|| {
// Water
if (wposf.z as f32) < water_height {
// Ocean
Some(water)
} else {
None
}
});
(block, height)
} else {
(None, sample.alt)
};
let block = structures
.iter()
.find_map(|st| {
let (st, st_sample) = st.as_ref()?;
st.get(index, wpos, st_sample)
})
.or(block);
block
})
.or_else(|| {
// Water
if (wposf.z as f32) < water_height {
// Ocean
Some(water)
} else {
None
}
})
}
}
pub struct ZCache<'a> {
wpos: Vec2<i32>,
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<i32>, Vec2<i32>),
volume: &'static Structure,
},
}
pub struct StructureInfo {
pos: Vec3<i32>,
seed: u32,
meta: StructureMeta,
}
impl StructureInfo {
fn get_bounds(&self) -> Aabb<i32> {
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<i32>, sample: &ColumnSample) -> Option<Block> {
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)
}
}

View File

@ -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<Option<ColumnSample<'a>>>,
st_pos: Vec2<i32>,
st_seed: u32,
st_sample: &ColumnSample,
index: IndexRef<'a>,
) -> Option<StructureInfo> {
// 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<Arc<Structure>> = Structure::load_group("oaks");
pub static ref OAK_STUMPS: Vec<Arc<Structure>> = Structure::load_group("oak_stumps");
pub static ref PINES: Vec<Arc<Structure>> = Structure::load_group("pines");
pub static ref PALMS: Vec<Arc<Structure>> = Structure::load_group("palms");
pub static ref SNOW_PINES: Vec<Arc<Structure>> = Structure::load_group("snow_pines");
pub static ref ACACIAS: Vec<Arc<Structure>> = Structure::load_group("acacias");
pub static ref FRUIT_TREES: Vec<Arc<Structure>> = Structure::load_group("fruit_trees");
pub static ref BIRCHES: Vec<Arc<Structure>> = Structure::load_group("birch");
pub static ref MANGROVE_TREES: Vec<Arc<Structure>> = Structure::load_group("mangrove_trees");
pub static ref QUIRKY: Vec<Arc<Structure>> = Structure::load_group("quirky");
pub static ref QUIRKY_DRY: Vec<Arc<Structure>> = Structure::load_group("quirky_dry");
}

96
world/src/canvas.rs Normal file
View File

@ -0,0 +1,96 @@
use crate::{
block::ZCache,
column::ColumnSample,
index::IndexRef,
sim::{SimChunk, WorldSim as Land},
util::Grid,
};
use common::{
terrain::{Block, TerrainChunk, TerrainChunkSize},
vol::{ReadVol, RectVolSize, WriteVol},
};
use std::ops::Deref;
use vek::*;
#[derive(Copy, Clone)]
pub struct CanvasInfo<'a> {
pub(crate) wpos: Vec2<i32>,
pub(crate) column_grid: &'a Grid<Option<ZCache<'a>>>,
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<i32> { self.wpos }
pub fn area(&self) -> Aabr<i32> {
Rect::from((
self.wpos(),
Extent2::from(TerrainChunkSize::RECT_SIZE.map(|e| e as i32)),
))
.into()
}
pub fn col(&self, pos: Vec2<i32>) -> Option<&'a ColumnSample> {
self.column_grid
.get(self.column_grid_border + pos - self.wpos())
.map(Option::as_ref)
.flatten()
.map(|zc| &zc.sample)
}
pub fn index(&self) -> IndexRef<'a> { self.index }
pub fn chunk(&self) -> &'a SimChunk { self.chunk }
pub fn land(&self) -> &'a Land { self.land }
}
pub struct Canvas<'a> {
pub(crate) info: CanvasInfo<'a>,
pub(crate) chunk: &'a mut TerrainChunk,
}
impl<'a> Canvas<'a> {
/// The borrow checker complains at immutable features of canvas (column
/// sampling, etc.) being used at the same time as mutable features
/// (writing blocks). To avoid this, this method extracts the
/// inner `CanvasInfo` such that it may be used independently.
pub fn info(&mut self) -> CanvasInfo<'a> { self.info }
pub fn get(&mut self, pos: Vec3<i32>) -> Option<Block> {
self.chunk.get(pos - self.wpos()).ok().copied()
}
pub fn set(&mut self, pos: Vec3<i32>, block: Block) {
let _ = self.chunk.set(pos - self.wpos(), block);
}
pub fn map(&mut self, pos: Vec3<i32>, 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<i32>, &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 }
}

View File

@ -646,16 +646,16 @@ fn walk_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>) -> Option<f32> {
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 {

View File

@ -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<i32>) -> Option<StructureData> {
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<i32>) -> [Option<StructureData>; 9] {
let mut metas = [None; 9];
self.sim
.gen_ctx
.structure_gen
.get(wpos)
.iter()
.copied()
.enumerate()
.for_each(|(i, (pos, seed))| {
metas[i] = self.get_local_structure(pos).or(Some(StructureData {
pos,
seed,
meta: None,
}));
});
metas
}
}
impl<'a> Sampler<'a> for ColumnGen<'a> {
@ -456,12 +405,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
river_overlap_distance_product / overlap_count
} as f32;
let cliff_hill = (sim
.gen_ctx
.small_nz
.get((wposf_turb.div(128.0)).into_array()) as f32)
.mul(4.0);
let riverless_alt_delta = (sim.gen_ctx.small_nz.get(
(wposf_turb.div(200.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64))).into_array(),
) as f32)
@ -491,9 +434,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
})
.unwrap_or(CONFIG.sea_level);
let is_cliffs = sim_chunk.is_cliffs;
let near_cliffs = sim_chunk.near_cliffs;
let river_gouge = 0.5;
let (_in_water, water_dist, alt_, water_level, riverless_alt, warp_factor) = if let Some(
(max_border_river_pos, river_chunk, max_border_river, max_border_river_dist),
@ -508,43 +448,44 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
max_border_river
.river_kind
.and_then(|river_kind| {
if let RiverKind::River { cross_section } = river_kind {
if max_border_river_dist.map(|(_, dist, _, _)| dist)
!= Some(Vec2::zero())
{
return None;
}
let (
_,
_,
river_width,
(river_t, (river_pos, _), downhill_river_chunk),
) = max_border_river_dist.unwrap();
let river_alt = Lerp::lerp(
river_chunk.alt.max(river_chunk.water_alt),
downhill_river_chunk.alt.max(downhill_river_chunk.water_alt),
river_t as f32,
);
let new_alt = river_alt - river_gouge;
let river_dist = wposf.distance(river_pos);
let river_height_factor = river_dist / (river_width * 0.5);
match river_kind {
RiverKind::River { cross_section } => {
if max_border_river_dist.map(|(_, dist, _, _)| dist)
!= Some(Vec2::zero())
{
return None;
}
let (
_,
_,
river_width,
(river_t, (river_pos, _), downhill_river_chunk),
) = max_border_river_dist.unwrap();
let river_alt = Lerp::lerp(
river_chunk.alt.max(river_chunk.water_alt),
downhill_river_chunk.alt.max(downhill_river_chunk.water_alt),
river_t as f32,
);
let new_alt = river_alt - river_gouge;
let river_dist = wposf.distance(river_pos);
let river_height_factor = river_dist / (river_width * 0.5);
let valley_alt = Lerp::lerp(
new_alt - cross_section.y.max(1.0),
new_alt - 1.0,
(river_height_factor * river_height_factor) as f32,
);
let valley_alt = Lerp::lerp(
new_alt - cross_section.y.max(1.0),
new_alt - 1.0,
(river_height_factor * river_height_factor) as f32,
);
Some((
true,
Some((river_dist - river_width * 0.5) as f32),
valley_alt,
new_alt,
river_alt,
0.0,
))
} else {
None
Some((
true,
Some((river_dist - river_width * 0.5) as f32),
valley_alt,
new_alt,
alt, //river_alt + cross_section.y.max(1.0),
0.0,
))
},
_ => None,
}
})
.unwrap_or_else(|| {
@ -693,7 +634,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
} else {
return (
true,
None,
Some(lake_dist as f32),
alt_for_river,
if in_bounds_ {
downhill_water_alt.max(lake_water_alt)
@ -726,7 +667,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
Some((river_dist - river_width * 0.5) as f32),
alt_for_river,
downhill_water_alt,
alt_for_river,
alt, //alt_for_river,
river_scale_factor as f32,
)
},
@ -737,7 +678,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
None,
alt_for_river,
downhill_water_alt,
alt_for_river,
alt, //alt_for_river,
river_scale_factor as f32,
))
});
@ -755,7 +696,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
None,
alt_for_river,
downhill_water_alt,
alt_for_river,
alt, //alt_for_river,
1.0,
)
};
@ -780,6 +721,26 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
.max(0.0)
.mul(8.0);
// Columns near water have a more stable temperature and so get pushed towards
// the average (0)
let temp = Lerp::lerp(
Lerp::lerp(temp, 0.0, 0.1),
temp,
water_dist
.map(|water_dist| water_dist / 20.0)
.unwrap_or(1.0)
.clamped(0.0, 1.0),
);
// Columns near water get a humidity boost
let humidity = Lerp::lerp(
Lerp::lerp(humidity, 1.0, 0.25),
humidity,
water_dist
.map(|water_dist| water_dist / 20.0)
.unwrap_or(1.0)
.clamped(0.0, 1.0),
);
let wposf3d = Vec3::new(wposf.x, wposf.y, alt as f64);
let marble_small = (sim.gen_ctx.hill_nz.get((wposf3d.div(3.0)).into_array()) as f32)
@ -918,7 +879,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
humidity
.sub(CONFIG.desert_hum)
.div(CONFIG.forest_hum.sub(CONFIG.desert_hum))
.mul(1.0),
.mul(1.25),
);
// From forest to jungle humidity, we go from snow to dark grass to grass to
// tropics to sand depending on temperature.
@ -986,15 +947,17 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
.max(-humidity.sub(CONFIG.desert_hum))
.mul(16.0)
.add((marble_small - 0.5) * 0.5);
let (alt, ground, sub_surface_color) = if snow_cover <= 0.5 && alt > water_level {
let (alt, ground, sub_surface_color, snow_cover) = if snow_cover <= 0.5 && alt > water_level
{
// Allow snow cover.
(
alt + 1.0 - snow_cover.max(0.0),
Rgb::lerp(snow, ground, snow_cover),
Lerp::lerp(sub_surface_color, ground, alt.sub(basement).mul(0.15)),
true,
)
} else {
(alt, ground, sub_surface_color)
(alt, ground, sub_surface_color, false)
};
// Make river banks not have grass
@ -1004,7 +967,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let near_ocean = max_river.and_then(|(_, _, river_data, _)| {
if (river_data.is_lake() || river_data.river_kind == Some(RiverKind::Ocean))
&& ((alt <= water_level.max(CONFIG.sea_level + 5.0) && !is_cliffs) || !near_cliffs)
&& alt <= water_level.max(CONFIG.sea_level + 5.0)
{
Some(water_level)
} else {
@ -1053,14 +1016,9 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
0.0
},
forest_kind: sim_chunk.forest_kind,
close_structures: self.gen_close_structures(wpos),
marble,
marble_small,
rock,
is_cliffs,
near_cliffs,
cliff_hill,
close_cliffs: sim.gen_ctx.cliff_gen.get(wpos),
temp,
humidity,
spawn_rate,
@ -1068,6 +1026,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
water_dist,
path,
cave,
snow_cover,
chunk: sim_chunk,
})
@ -1086,14 +1045,9 @@ pub struct ColumnSample<'a> {
pub sub_surface_color: Rgb<f32>,
pub tree_density: f32,
pub forest_kind: ForestKind,
pub close_structures: [Option<StructureData>; 9],
pub marble: f32,
pub marble_small: f32,
pub rock: f32,
pub is_cliffs: bool,
pub near_cliffs: bool,
pub cliff_hill: f32,
pub close_cliffs: [(Vec2<i32>, u32); 9],
pub temp: f32,
pub humidity: f32,
pub spawn_rate: f32,
@ -1101,13 +1055,7 @@ pub struct ColumnSample<'a> {
pub water_dist: Option<f32>,
pub path: Option<(f32, Vec2<f32>, Path, Vec2<f32>)>,
pub cave: Option<(f32, Vec2<f32>, Cave, Vec2<f32>)>,
pub snow_cover: bool,
pub chunk: &'a SimChunk,
}
#[derive(Copy, Clone)]
pub struct StructureData {
pub pos: Vec2<i32>,
pub seed: u32,
pub meta: Option<StructureMeta>,
}

View File

@ -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,

View File

@ -1,11 +1,12 @@
pub mod scatter;
pub mod tree;
pub use self::scatter::apply_scatter_to;
pub use self::{scatter::apply_scatter_to, tree::apply_trees_to};
use crate::{
column::ColumnSample,
util::{RandomField, Sampler},
IndexRef,
Canvas, IndexRef,
};
use common::{
assets::Asset,
@ -32,180 +33,162 @@ pub struct Colors {
const EMPTY_AIR: Block = Block::air(SpriteKind::Empty);
pub fn apply_paths_to<'a>(
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
index: IndexRef,
) {
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
pub fn apply_paths_to(canvas: &mut Canvas) {
let info = canvas.info();
canvas.foreach_col(|canvas, wpos2d, col| {
let surface_z = col.riverless_alt.floor() as i32;
let wpos2d = wpos2d + offs;
let noisy_color = |color: Rgb<u8>, factor: u32| {
let nz = RandomField::new(0).get(Vec3::new(wpos2d.x, wpos2d.y, surface_z));
color.map(|e| {
(e as u32 + nz % (factor * 2))
.saturating_sub(factor)
.min(255) as u8
})
};
// Sample terrain
let col_sample = if let Some(col_sample) = get_column(offs) {
col_sample
} else {
continue;
if let Some((path_dist, path_nearest, path, _)) =
col.path.filter(|(dist, _, path, _)| *dist < path.width)
{
let inset = 0;
// Try to use the column at the centre of the path for sampling to make them
// flatter
let col_pos = -info.wpos().map(|e| e as f32) + path_nearest;
let col00 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0));
let col10 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0));
let col01 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1));
let col11 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1));
let col_attr = |col: &ColumnSample| {
Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0))
};
let surface_z = col_sample.riverless_alt.floor() as i32;
let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) {
(Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp(
Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()),
Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()),
path_nearest.y.fract(),
),
_ => col_attr(col),
}
.into_array();
let (bridge_offset, depth) = (
((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0,
((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs())
* (riverless_alt + 5.0 - alt).max(0.0)
* 1.75
+ 3.0) as i32,
);
let surface_z = (riverless_alt + bridge_offset).floor() as i32;
let noisy_color = |col: Rgb<u8>, factor: u32| {
let nz = RandomField::new(0).get(Vec3::new(wpos2d.x, wpos2d.y, surface_z));
col.map(|e| {
(e as u32 + nz % (factor * 2))
.saturating_sub(factor)
.min(255) as u8
})
};
if let Some((path_dist, path_nearest, path, _)) = col_sample
.path
.filter(|(dist, _, path, _)| *dist < path.width)
{
let inset = 0;
// Try to use the column at the centre of the path for sampling to make them
// flatter
let col_pos = (offs - wpos2d).map(|e| e as f32) + path_nearest;
let col00 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0));
let col10 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0));
let col01 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1));
let col11 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1));
let col_attr = |col: &ColumnSample| {
Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0))
};
let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) {
(Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp(
Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()),
Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()),
path_nearest.y.fract(),
),
_ => col_attr(col_sample),
}
.into_array();
let (bridge_offset, depth) = (
((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0,
((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs())
* (riverless_alt + 5.0 - alt).max(0.0)
* 1.75
+ 3.0) as i32,
for z in inset - depth..inset {
let _ = canvas.set(
Vec3::new(wpos2d.x, wpos2d.y, surface_z + z),
if bridge_offset >= 2.0 && path_dist >= 3.0 || z < inset - 1 {
Block::new(
BlockKind::Rock,
noisy_color(info.index().colors.layer.bridge.into(), 8),
)
} else {
let path_color =
path.surface_color(col.sub_surface_color.map(|e| (e * 255.0) as u8));
Block::new(BlockKind::Earth, noisy_color(path_color, 8))
},
);
let surface_z = (riverless_alt + bridge_offset).floor() as i32;
for z in inset - depth..inset {
let _ = vol.set(
Vec3::new(offs.x, offs.y, surface_z + z),
if bridge_offset >= 2.0 && path_dist >= 3.0 || z < inset - 1 {
Block::new(
BlockKind::Rock,
noisy_color(index.colors.layer.bridge.into(), 8),
)
} else {
let path_color = path.surface_color(
col_sample.sub_surface_color.map(|e| (e * 255.0) as u8),
);
Block::new(BlockKind::Earth, noisy_color(path_color, 8))
},
);
}
let head_space = path.head_space(path_dist);
for z in inset..inset + head_space {
let pos = Vec3::new(offs.x, offs.y, surface_z + z);
if vol.get(pos).unwrap().kind() != BlockKind::Water {
let _ = vol.set(pos, EMPTY_AIR);
}
}
let head_space = path.head_space(path_dist);
for z in inset..inset + head_space {
let pos = Vec3::new(wpos2d.x, wpos2d.y, surface_z + z);
if canvas.get(pos).unwrap().kind() != BlockKind::Water {
let _ = canvas.set(pos, EMPTY_AIR);
}
}
}
}
});
}
pub fn apply_caves_to<'a>(
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
index: IndexRef,
) {
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
pub fn apply_caves_to(canvas: &mut Canvas) {
let info = canvas.info();
canvas.foreach_col(|canvas, wpos2d, col| {
let surface_z = col.riverless_alt.floor() as i32;
let wpos2d = wpos2d + offs;
if let Some((cave_dist, _, cave, _)) =
col.cave.filter(|(dist, _, cave, _)| *dist < cave.width)
{
let cave_x = (cave_dist / cave.width).min(1.0);
// Sample terrain
let col_sample = if let Some(col_sample) = get_column(offs) {
col_sample
} else {
continue;
};
let surface_z = col_sample.riverless_alt.floor() as i32;
// Relative units
let cave_floor = 0.0 - 0.5 * (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width;
let cave_height = (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width;
if let Some((cave_dist, _, cave, _)) = col_sample
.cave
.filter(|(dist, _, cave, _)| *dist < cave.width)
{
let cave_x = (cave_dist / cave.width).min(1.0);
// Abs units
let cave_base = (cave.alt + cave_floor) as i32;
let cave_roof = (cave.alt + cave_height) as i32;
// Relative units
let cave_floor = 0.0 - 0.5 * (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width;
let cave_height = (1.0 - cave_x.powf(2.0)).max(0.0).sqrt() * cave.width;
// Abs units
let cave_base = (cave.alt + cave_floor) as i32;
let cave_roof = (cave.alt + cave_height) as i32;
for z in cave_base..cave_roof {
if cave_x < 0.95
|| index.noise.cave_nz.get(
Vec3::new(wpos2d.x, wpos2d.y, z)
.map(|e| e as f64 * 0.15)
.into_array(),
) < 0.0
{
let _ = vol.set(Vec3::new(offs.x, offs.y, z), EMPTY_AIR);
}
}
// Stalagtites
let stalagtites = index
.noise
.cave_nz
.get(wpos2d.map(|e| e as f64 * 0.125).into_array())
.sub(0.5)
.max(0.0)
.mul(
(col_sample.alt - cave_roof as f32 - 5.0)
.mul(0.15)
.clamped(0.0, 1.0) as f64,
)
.mul(45.0) as i32;
for z in cave_roof - stalagtites..cave_roof {
let _ = vol.set(
Vec3::new(offs.x, offs.y, z),
Block::new(BlockKind::WeakRock, index.colors.layer.stalagtite.into()),
);
}
let cave_depth = (col_sample.alt - cave.alt).max(0.0);
let difficulty = cave_depth / 100.0;
// Scatter things in caves
if RandomField::new(index.seed).chance(wpos2d.into(), 0.001 * difficulty.powf(1.5))
&& cave_base < surface_z as i32 - 25
for z in cave_base..cave_roof {
if cave_x < 0.95
|| info.index().noise.cave_nz.get(
Vec3::new(wpos2d.x, wpos2d.y, z)
.map(|e| e as f64 * 0.15)
.into_array(),
) < 0.0
{
let kind = *Lottery::<SpriteKind>::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::<SpriteKind>::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>(

View File

@ -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<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
index: IndexRef,
chunk: &SimChunk,
) {
pub fn apply_scatter_to(canvas: &mut Canvas) {
use SpriteKind::*;
#[allow(clippy::type_complexity)]
// TODO: Add back all sprites we had before
@ -286,76 +277,65 @@ pub fn apply_scatter_to<'a>(
(Chest, true, |_, _| (MUSH_FACT * 0.1, None)),
];
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
canvas.foreach_col(|canvas, wpos2d, col| {
let underwater = col.water_level > col.alt;
let wpos2d = wpos2d + offs;
// Sample terrain
let col_sample = if let Some(col_sample) = get_column(offs) {
col_sample
} else {
continue;
};
let underwater = col_sample.water_level > col_sample.alt;
let kind = scatter
.iter()
.enumerate()
.find_map(|(i, (kind, is_underwater, f))| {
let (density, patch) = f(chunk, col_sample);
let is_patch = patch
.map(|(wavelen, threshold)| {
index
.noise
.scatter_nz
.get(
wpos2d
.map(|e| e as f64 / wavelen as f64 + i as f64 * 43.0)
.into_array(),
)
.abs()
> 1.0 - threshold as f64
})
.unwrap_or(true);
if density > 0.0
&& is_patch
&& RandomField::new(i as u32)
.chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density)
&& underwater == *is_underwater
{
Some(*kind)
} else {
None
}
});
if let Some(kind) = kind {
let alt = col_sample.alt as i32;
// Find the intersection between ground and air, if there is one near the
// surface
if let Some(solid_end) = (-4..8)
.find(|z| {
vol.get(Vec3::new(offs.x, offs.y, alt + z))
.map(|b| b.is_solid())
.unwrap_or(false)
})
.and_then(|solid_start| {
(1..8).map(|z| solid_start + z).find(|z| {
vol.get(Vec3::new(offs.x, offs.y, alt + z))
.map(|b| !b.is_solid())
.unwrap_or(true)
})
let kind = scatter
.iter()
.enumerate()
.find_map(|(i, (kind, is_underwater, f))| {
let (density, patch) = f(canvas.chunk(), col);
let is_patch = patch
.map(|(wavelen, threshold)| {
canvas
.index()
.noise
.scatter_nz
.get(
wpos2d
.map(|e| e as f64 / wavelen as f64 + i as f64 * 43.0)
.into_array(),
)
.abs()
> 1.0 - threshold as f64
})
.unwrap_or(true);
if density > 0.0
&& is_patch
&& RandomField::new(i as u32).chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density)
&& underwater == *is_underwater
{
let _ = vol.map(Vec3::new(offs.x, offs.y, alt + solid_end), |block| {
block.with_sprite(kind)
});
Some(*kind)
} else {
None
}
});
if let Some(kind) = kind {
let alt = col.alt as i32;
// Find the intersection between ground and air, if there is one near the
// surface
if let Some(solid_end) = (-4..8)
.find(|z| {
canvas
.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))
.map(|b| b.is_solid())
.unwrap_or(false)
})
.and_then(|solid_start| {
(1..8).map(|z| solid_start + z).find(|z| {
canvas
.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))
.map(|b| !b.is_solid())
.unwrap_or(true)
})
})
{
canvas.map(Vec3::new(wpos2d.x, wpos2d.y, alt + solid_end), |block| {
block.with_sprite(kind)
});
}
}
}
});
}

145
world/src/layer/tree.rs Normal file
View File

@ -0,0 +1,145 @@
use crate::{
all::ForestKind,
block::block_from_structure,
column::ColumnGen,
util::{RandomPerm, Sampler, UnitChooser},
Canvas, CONFIG,
};
use common::{
terrain::{structure::Structure, Block, BlockKind},
vol::ReadVol,
};
use lazy_static::lazy_static;
use std::{collections::HashMap, f32, sync::Arc};
use vek::*;
lazy_static! {
pub static ref OAKS: Vec<Arc<Structure>> = Structure::load_group("oaks");
pub static ref OAK_STUMPS: Vec<Arc<Structure>> = Structure::load_group("oak_stumps");
pub static ref PINES: Vec<Arc<Structure>> = Structure::load_group("pines");
pub static ref PALMS: Vec<Arc<Structure>> = Structure::load_group("palms");
pub static ref ACACIAS: Vec<Arc<Structure>> = Structure::load_group("acacias");
pub static ref FRUIT_TREES: Vec<Arc<Structure>> = Structure::load_group("fruit_trees");
pub static ref BIRCHES: Vec<Arc<Structure>> = Structure::load_group("birch");
pub static ref MANGROVE_TREES: Vec<Arc<Structure>> = Structure::load_group("mangrove_trees");
pub static ref QUIRKY: Vec<Arc<Structure>> = Structure::load_group("quirky");
pub static ref QUIRKY_DRY: Vec<Arc<Structure>> = Structure::load_group("quirky_dry");
}
static MODEL_RAND: RandomPerm = RandomPerm::new(0xDB21C052);
static UNIT_CHOOSER: UnitChooser = UnitChooser::new(0x700F4EC7);
static QUIRKY_RAND: RandomPerm = RandomPerm::new(0xA634460F);
pub fn apply_trees_to(canvas: &mut Canvas) {
struct Tree {
pos: Vec3<i32>,
model: Arc<Structure>,
seed: u32,
units: (Vec2<i32>, Vec2<i32>),
}
let mut tree_cache = HashMap::new();
let info = canvas.info();
canvas.foreach_col(|canvas, wpos2d, col| {
let trees = info.land().get_near_trees(wpos2d);
for (tree_wpos, seed) in trees {
let tree = if let Some(tree) = tree_cache.entry(tree_wpos).or_insert_with(|| {
let col = ColumnGen::new(info.land()).get((tree_wpos, info.index()))?;
// Ensure that it's valid to place a tree here
if ((seed.wrapping_mul(13)) & 0xFF) as f32 / 256.0 > col.tree_density
|| col.alt < col.water_level
|| col.spawn_rate < 0.5
|| col.water_dist.map(|d| d < 8.0).unwrap_or(false)
|| col.path.map(|(d, _, _, _)| d < 12.0).unwrap_or(false)
{
return None;
}
Some(Tree {
pos: Vec3::new(tree_wpos.x, tree_wpos.y, col.alt as i32),
model: {
let models: &'static [_] = if QUIRKY_RAND.get(seed) % 512 == 17 {
if col.temp > CONFIG.desert_temp {
&QUIRKY_DRY
} else {
&QUIRKY
}
} else {
match col.forest_kind {
ForestKind::Oak if QUIRKY_RAND.get(seed) % 16 == 7 => &OAK_STUMPS,
ForestKind::Oak if QUIRKY_RAND.get(seed) % 19 == 7 => &FRUIT_TREES,
ForestKind::Palm => &PALMS,
ForestKind::Savannah => &ACACIAS,
ForestKind::Oak => &OAKS,
ForestKind::Pine => &PINES,
ForestKind::Birch => &BIRCHES,
ForestKind::Mangrove => &MANGROVE_TREES,
}
};
Arc::clone(
&models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize
% models.len()],
)
},
seed,
units: UNIT_CHOOSER.get(seed),
})
}) {
tree
} else {
continue;
};
let bounds = tree.model.get_bounds();
let mut is_top = true;
let mut is_leaf_top = true;
for z in (bounds.min.z..bounds.max.z).rev() {
let wpos = Vec3::new(wpos2d.x, wpos2d.y, tree.pos.z + z);
let model_pos = Vec3::from(
(wpos - tree.pos)
.xy()
.map2(Vec2::new(tree.units.0, tree.units.1), |rpos, unit| {
unit * rpos
})
.sum(),
) + Vec3::unit_z() * (wpos.z - tree.pos.z);
block_from_structure(
info.index(),
if let Some(block) = tree.model.get(model_pos).ok().copied() {
block
} else {
// If we hit an inaccessible block, we're probably outside the model bounds.
// Skip this column.
break;
},
wpos,
tree.pos.xy(),
tree.seed,
col,
Block::air,
)
.map(|block| {
// Add a snow covering to the block above under certain circumstances
if col.snow_cover
&& ((block.kind() == BlockKind::Leaves && is_leaf_top)
|| (is_top && block.is_filled()))
{
canvas.set(
wpos + Vec3::unit_z(),
Block::new(BlockKind::Snow, Rgb::new(210, 210, 255)),
);
}
canvas.set(wpos, block);
is_leaf_top = false;
is_top = false;
})
.unwrap_or_else(|| {
is_leaf_top = true;
});
}
}
});
}

View File

@ -8,11 +8,13 @@
const_generics,
const_panic,
label_break_value,
or_patterns
or_patterns,
array_value_iter
)]
mod all;
mod block;
pub mod canvas;
pub mod civ;
mod column;
pub mod config;
@ -25,7 +27,10 @@ pub mod site;
pub mod util;
// Reexports
pub use crate::config::CONFIG;
pub use crate::{
canvas::{Canvas, CanvasInfo},
config::CONFIG,
};
pub use block::BlockGen;
pub use column::ColumnSample;
pub use index::{IndexOwned, IndexRef};
@ -126,7 +131,6 @@ impl World {
);
let water = Block::new(BlockKind::Water, Rgb::zero());
let _chunk_size2d = TerrainChunkSize::RECT_SIZE;
let (base_z, sim_chunk) = match self
.sim
/*.get_interpolated(
@ -168,8 +172,7 @@ impl World {
_ => continue,
};
let (min_z, only_structures_min_z, max_z) =
z_cache.get_z_limits(&mut sampler, index);
let (min_z, max_z) = z_cache.get_z_limits();
(base_z..min_z as i32).for_each(|z| {
let _ = chunk.set(Vec3::new(x, y, z), stone);
@ -178,11 +181,8 @@ impl World {
(min_z as i32..max_z as i32).for_each(|z| {
let lpos = Vec3::new(x, y, z);
let wpos = Vec3::from(chunk_wpos2d) + lpos;
let only_structures = lpos.z >= only_structures_min_z as i32;
if let Some(block) =
sampler.get_with_z_cache(wpos, Some(&z_cache), only_structures, index)
{
if let Some(block) = sampler.get_with_z_cache(wpos, Some(&z_cache)) {
let _ = chunk.set(lpos, block);
}
});
@ -201,9 +201,22 @@ impl World {
let mut dynamic_rng = rand::thread_rng();
// Apply layers (paths, caves, etc.)
layer::apply_caves_to(chunk_wpos2d, sample_get, &mut chunk, index);
layer::apply_scatter_to(chunk_wpos2d, sample_get, &mut chunk, index, sim_chunk);
layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk, index);
let mut canvas = Canvas {
info: CanvasInfo {
wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
column_grid: &zcache_grid,
column_grid_border: grid_border,
land: &self.sim,
index,
chunk: sim_chunk,
},
chunk: &mut chunk,
};
layer::apply_trees_to(&mut canvas);
layer::apply_scatter_to(&mut canvas);
layer::apply_caves_to(&mut canvas);
layer::apply_paths_to(&mut canvas);
// Apply site generation
sim_chunk.sites.iter().for_each(|site| {

View File

@ -28,7 +28,10 @@ use crate::{
civ::Place,
column::ColumnGen,
site::Site,
util::{seed_expan, FastNoise, RandomField, Sampler, StructureGen2d, LOCALITY, NEIGHBORS},
util::{
seed_expan, FastNoise, RandomField, RandomPerm, Sampler, StructureGen2d, LOCALITY,
NEIGHBORS,
},
IndexRef, CONFIG,
};
use common::{
@ -103,7 +106,6 @@ pub(crate) struct GenCtx {
// Small amounts of noise for simulating rough terrain.
pub small_nz: BasicMulti,
pub rock_nz: HybridMulti,
pub cliff_nz: HybridMulti,
pub warp_nz: FastNoise,
pub tree_nz: BasicMulti,
@ -112,7 +114,6 @@ pub(crate) struct GenCtx {
pub structure_gen: StructureGen2d,
pub region_gen: StructureGen2d,
pub cliff_gen: StructureGen2d,
pub fast_turb_x_nz: FastNoise,
pub fast_turb_y_nz: FastNoise,
@ -503,7 +504,6 @@ impl WorldSim {
small_nz: BasicMulti::new().set_octaves(2).set_seed(rng.gen()),
rock_nz: HybridMulti::new().set_persistence(0.3).set_seed(rng.gen()),
cliff_nz: HybridMulti::new().set_persistence(0.3).set_seed(rng.gen()),
warp_nz: FastNoise::new(rng.gen()),
tree_nz: BasicMulti::new()
.set_octaves(12)
@ -514,7 +514,6 @@ impl WorldSim {
structure_gen: StructureGen2d::new(rng.gen(), 32, 16),
region_gen: StructureGen2d::new(rng.gen(), 400, 96),
cliff_gen: StructureGen2d::new(rng.gen(), 80, 56),
humid_nz: Billow::new()
.set_octaves(9)
.set_persistence(0.4)
@ -1431,25 +1430,11 @@ impl WorldSim {
.into_par_iter()
.map_init(
|| Box::new(BlockGen::new(ColumnGen::new(self))),
|block_gen, posi| {
let wpos = uniform_idx_as_vec2(self.map_size_lg(), posi);
let mut sample = column_sample.get(
|_block_gen, posi| {
let sample = column_sample.get(
(uniform_idx_as_vec2(self.map_size_lg(), posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
index)
)?;
let alt = sample.alt;
/* let z_cache = block_gen.get_z_cache(wpos);
sample.alt = alt.max(z_cache.get_z_limits(&mut block_gen).2); */
sample.alt = alt.max(BlockGen::get_cliff_height(
&block_gen.column_gen,
&mut block_gen.column_cache,
wpos.map(|e| e as f32),
&sample.close_cliffs,
sample.cliff_hill,
32.0,
index,
));
sample.basement += sample.alt - alt;
// sample.water_level = CONFIG.sea_level.max(sample.water_level);
Some(sample)
@ -1997,6 +1982,14 @@ impl WorldSim {
pub fn get_nearest_cave(&self, wpos: Vec2<i32>) -> Option<(f32, Vec2<f32>, Cave, Vec2<f32>)> {
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<i32>) -> impl Iterator<Item = (Vec2<i32>, u32)> + '_ {
// Deterministic based on wpos
std::array::IntoIter::new(self.gen_ctx.structure_gen.get(wpos))
}
}
#[derive(Debug)]
@ -2010,8 +2003,6 @@ pub struct SimChunk {
pub temp: f32,
pub humidity: f32,
pub rockiness: f32,
pub is_cliffs: bool,
pub near_cliffs: bool,
pub tree_density: f32,
pub forest_kind: ForestKind,
pub spawn_rate: f32,
@ -2068,11 +2059,6 @@ impl SimChunk {
// Even less granular--if this matters we can make the sign affect the quantity slightly.
let abs_lat_uniform = latitude_uniform.abs(); */
// Take the weighted average of our randomly generated base humidity, and the
// calculated water flux over this point in order to compute humidity.
const HUMID_WEIGHTS: [f32; 2] = [2.0, 1.0];
let humidity = cdf_irwin_hall(&HUMID_WEIGHTS, [humid_uniform, flux_uniform]);
// We also correlate temperature negatively with altitude and absolute latitude,
// using different weighting than we use for humidity.
const TEMP_WEIGHTS: [f32; 2] = [/* 1.5, */ 1.0, 2.0];
@ -2087,6 +2073,18 @@ impl SimChunk {
.sub(0.5)
.mul(2.0);
// Take the weighted average of our randomly generated base humidity, and the
// calculated water flux over this point in order to compute humidity.
const HUMID_WEIGHTS: [f32; 3] = [1.0, 1.0, 0.75];
let humidity = cdf_irwin_hall(&HUMID_WEIGHTS, [humid_uniform, flux_uniform, 1.0]);
// Moisture evaporates more in hot places
let humidity = humidity
* (1.0
- (temp - CONFIG.tropical_temp)
.max(0.0)
.div(1.0 - CONFIG.tropical_temp))
.max(0.0);
let mut alt = CONFIG.sea_level.add(alt_pre);
let basement = CONFIG.sea_level.add(basement_pre);
let water_alt = CONFIG.sea_level.add(water_alt_pre);
@ -2101,10 +2099,6 @@ impl SimChunk {
)
};
//let cliff = gen_ctx.cliff_nz.get((wposf.div(2048.0)).into_array()) as f32 +
// chaos * 0.2;
let cliff = 0.0; // Disable cliffs
// Logistic regression. Make sure x ∈ (0, 1).
let logit = |x: f64| x.ln() - x.neg().ln_1p();
// 0.5 + 0.5 * tanh(ln(1 / (1 - 0.1) - 1) / (2 * (sqrt(3)/pi)))
@ -2154,7 +2148,6 @@ impl SimChunk {
.mul(1.5)
.add(1.0)
.mul(0.5)
.mul(1.2 - chaos as f64 * 0.95)
.add(0.05)
.max(0.0)
.min(1.0);
@ -2165,13 +2158,31 @@ impl SimChunk {
1.0
} else {
// Weighted logit sum.
logistic_cdf(logit(humidity as f64) + 0.5 * logit(tree_density))
logistic_cdf(logit(tree_density))
}
// rescale to (-0.95, 0.95)
.sub(0.5)
.mul(0.95)
.add(0.5)
} as f32;
const MIN_TREE_HUM: f32 = 0.15;
// Tree density increases exponentially with humidity...
let tree_density = (tree_density * (humidity - MIN_TREE_HUM).max(0.0).mul(1.0 + MIN_TREE_HUM) / temp.max(0.75))
// ...but is ultimately limited by available sunlight (and our tree generation system)
.min(1.0);
// Sand dunes (formed over a short period of time)
let alt = alt
+ if river.near_water() {
0.0
} else {
let warp = Vec2::new(
gen_ctx.turb_x_nz.get(wposf.div(256.0).into_array()) as f32,
gen_ctx.turb_y_nz.get(wposf.div(256.0).into_array()) as f32,
) * 192.0;
let dune_nz = (wposf.map(|e| e as f32) + warp).sum().div(100.0).sin() * 0.5 + 0.5;
let dune_scale = 16.0;
dune_nz * dune_scale * (temp - 0.75).clamped(0.0, 0.25) * 4.0
};
Self {
chaos,
@ -2191,67 +2202,71 @@ impl SimChunk {
} else {
0.0
},
is_cliffs: cliff > 0.5 && !is_underwater,
near_cliffs: cliff > 0.2,
tree_density,
forest_kind: if temp > CONFIG.temperate_temp {
if temp > CONFIG.desert_temp {
if humidity > CONFIG.jungle_hum {
// Forests in desert temperatures with extremely high humidity
// should probably be different from palm trees, but we use them
// for now.
ForestKind::Palm
} else if humidity > CONFIG.forest_hum {
ForestKind::Palm
} else if humidity > CONFIG.desert_hum {
// Low but not desert humidity, so we should really have some other
// terrain...
ForestKind::Savannah
} else {
ForestKind::Savannah
}
} else if temp > CONFIG.tropical_temp {
if humidity > CONFIG.jungle_hum {
if tree_density > 0.0 {
// println!("Mangrove: {:?}", wposf);
}
ForestKind::Mangrove
} else if humidity > CONFIG.forest_hum {
// NOTE: Probably the wrong kind of tree for this climate.
ForestKind::Oak
} else if humidity > CONFIG.desert_hum {
// Low but not desert... need something besides savannah.
ForestKind::Savannah
} else {
ForestKind::Savannah
}
} else if humidity > CONFIG.jungle_hum {
// Temperate climate with jungle humidity...
// https://en.wikipedia.org/wiki/Humid_subtropical_climates are often
// densely wooded and full of water. Semitropical rainforests, basically.
// For now we just treat them like other rainforests.
ForestKind::Oak
} else if humidity > CONFIG.forest_hum {
// Moderate climate, moderate humidity.
ForestKind::Oak
} else if humidity > CONFIG.desert_hum {
// With moderate temperature and low humidity, we should probably see
// something different from savannah, but oh well...
ForestKind::Savannah
} else {
ForestKind::Savannah
}
} else {
// For now we don't take humidity into account for cold climates (but we really
// should!) except that we make sure we only have snow pines when there is snow.
if temp <= CONFIG.snow_temp {
ForestKind::SnowPine
} else if humidity > CONFIG.desert_hum {
ForestKind::Pine
} else {
// Should really have something like tundra.
ForestKind::Pine
}
forest_kind: {
// Whittaker diagram
let candidates = [
// A smaller prevalence means that the range of values this tree appears in
// will shrink compared to neighbouring trees in the
// topology of the Whittaker diagram.
// Humidity, temperature, near_water, each with prevalence
(
ForestKind::Palm,
(CONFIG.desert_hum, 1.5),
(CONFIG.tropical_temp, 1.5),
(1.0, 2.0),
),
(
ForestKind::Savannah,
(CONFIG.desert_hum, 2.0),
(CONFIG.tropical_temp, 1.5),
(0.0, 1.0),
),
(
ForestKind::Oak,
(CONFIG.forest_hum, 1.5),
(0.0, 1.5),
(0.0, 1.0),
),
(
ForestKind::Mangrove,
(CONFIG.jungle_hum, 0.5),
(CONFIG.tropical_temp, 0.5),
(0.0, 1.0),
),
(
ForestKind::Pine,
(CONFIG.forest_hum, 1.25),
(CONFIG.snow_temp, 2.5),
(0.0, 1.0),
),
(
ForestKind::Birch,
(CONFIG.desert_hum, 1.5),
(CONFIG.temperate_temp, 1.5),
(0.0, 1.0),
),
];
candidates
.iter()
.enumerate()
.min_by_key(|(i, (_, (h, h_prev), (t, t_prev), (w, w_prev)))| {
let rand = RandomPerm::new(*i as u32 * 1000);
let noise =
Vec3::iota().map(|e| (rand.get(e) & 0xFF) as f32 / 255.0 - 0.5) * 2.0;
(Vec3::new(
(*h - humidity) / *h_prev,
(*t - temp) / *t_prev,
(*w - if river.near_water() { 1.0 } else { 0.0 }) / *w_prev,
)
.add(noise * 0.1)
.map(|e| e * e)
.sum()
* 10000.0) as i32
})
.map(|(_, c)| c.0)
.unwrap() // Can't fail
},
spawn_rate: 1.0,
river,

View File

@ -93,7 +93,7 @@ pub fn center_of(p: [Vec2<f32>; 3]) -> Vec2<f32> {
impl WorldSim {
fn can_host_settlement(&self, pos: Vec2<i32>) -> 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)