Merge branch 'sharp/jungle' into 'master'

Sharp/jungle

See merge request veloren/veloren!447
This commit is contained in:
Joshua Barretto 2019-08-22 23:40:46 +00:00
commit f7f0958d41
12 changed files with 654 additions and 103 deletions

BIN
assets/world/tree/mangroves/4.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/world/tree/mangroves/5.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/world/tree/mangroves/6.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/world/tree/mangroves/7.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/world/tree/mangroves/8.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,8 +1,9 @@
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub enum ForestKind {
Palm,
Savannah,
Oak,
Pine,
SnowPine,
Mangrove,
}

View File

@ -58,6 +58,7 @@ pub fn structure_gen<'a>(
ForestKind::Oak => &OAKS,
ForestKind::Pine => &PINES,
ForestKind::SnowPine => &SNOW_PINES,
ForestKind::Mangrove => &MANGROVE_TREES,
}
};
@ -421,6 +422,34 @@ lazy_static! {
];
*/
pub static ref MANGROVE_TREES: Vec<Arc<Structure>> = vec![
// oak stumps
assets::load_map("world.tree.mangroves.1", |s: Structure| s
.with_center(Vec3::new(18, 18, 8)))
.unwrap(),
assets::load_map("world.tree.mangroves.2", |s: Structure| s
.with_center(Vec3::new(16, 17, 7)))
.unwrap(),
assets::load_map("world.tree.mangroves.3", |s: Structure| s
.with_center(Vec3::new(18, 18, 8)))
.unwrap(),
assets::load_map("world.tree.mangroves.4", |s: Structure| s
.with_center(Vec3::new(18, 16, 8)))
.unwrap(),
assets::load_map("world.tree.mangroves.5", |s: Structure| s
.with_center(Vec3::new(16, 17, 8)))
.unwrap(),
assets::load_map("world.tree.mangroves.6", |s: Structure| s
.with_center(Vec3::new(18, 18, 8)))
.unwrap(),
assets::load_map("world.tree.mangroves.7", |s: Structure| s
.with_center(Vec3::new(18, 17, 8)))
.unwrap(),
assets::load_map("world.tree.mangroves.8", |s: Structure| s
.with_center(Vec3::new(18, 18, 8)))
.unwrap(),
];
pub static ref QUIRKY: Vec<Arc<Structure>> = vec![
st_asset("world.structure.natural.tower-ruin", (11, 14, 5)),
st_asset("world.structure.natural.witch-hut", (10, 13, 9)),
@ -431,4 +460,6 @@ lazy_static! {
st_asset("world.structure.natural.ribcage-large", (13, 19, 8)),
st_asset("world.structure.natural.skull-large", (15, 20, 4)),
];
}

View File

@ -76,7 +76,11 @@ impl<'a> ColumnGen<'a> {
});
let chunk = self.world.sim().get(chunk_pos)?;
if seed % 5 == 2 && chunk.temp > CONFIG.desert_temp && chunk.alt > CONFIG.sea_level + 5.0 {
if seed % 5 == 2
&& chunk.temp > CONFIG.desert_temp
&& chunk.humidity < CONFIG.desert_hum
&& chunk.alt > CONFIG.sea_level + 5.0
{
Some(StructureData {
pos,
seed,
@ -139,6 +143,7 @@ impl<'a> Sampler for ColumnGen<'a> {
let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?;
let temp = sim.get_interpolated(wpos, |chunk| chunk.temp)?;
let dryness = sim.get_interpolated(wpos, |chunk| chunk.dryness)?;
let humidity = sim.get_interpolated(wpos, |chunk| chunk.humidity)?;
let rockiness = sim.get_interpolated(wpos, |chunk| chunk.rockiness)?;
let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?;
let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?;
@ -158,11 +163,17 @@ impl<'a> Sampler for ColumnGen<'a> {
*/
let river = 0.0;
let cliff_hill =
(sim.gen_ctx.small_nz.get((wposf.div(128.0)).into_array()) as f32).mul(16.0);
let cliff_hill = (sim
.gen_ctx
.small_nz
.get((wposf_turb.div(128.0)).into_array()) as f32)
.mul(24.0);
let riverless_alt = sim.get_interpolated(wpos, |chunk| chunk.alt)?
+ (sim.gen_ctx.small_nz.get((wposf.div(256.0)).into_array()) as f32)
+ (sim
.gen_ctx
.small_nz
.get((wposf_turb.div(150.0)).into_array()) as f32)
.abs()
.mul(chaos.max(0.15))
.mul(64.0);
@ -201,41 +212,168 @@ impl<'a> Sampler for ColumnGen<'a> {
.mul(0.5)
.add(marble_small.sub(0.5).mul(0.25));
let temp = temp.add((marble - 0.5) * 0.25);
let humidity = humidity.add((marble - 0.5) * 0.25);
// Colours
let cold_grass = Rgb::new(0.0, 0.49, 0.42);
let cold_grass = Rgb::new(0.0, 0.5, 0.25);
let warm_grass = Rgb::new(0.03, 0.8, 0.0);
let dark_grass = Rgb::new(0.01, 0.3, 0.0);
let wet_grass = Rgb::new(0.1, 0.8, 0.2);
let cold_stone = Rgb::new(0.57, 0.67, 0.8);
let warm_stone = Rgb::new(0.77, 0.77, 0.64);
let beach_sand = Rgb::new(0.89, 0.87, 0.64);
let desert_sand = Rgb::new(0.93, 0.80, 0.54);
let snow = Rgb::broadcast(0.77);
let snow = Rgb::new(0.8, 0.85, 1.0);
let dirt = Lerp::lerp(
Rgb::new(0.078, 0.078, 0.20),
Rgb::new(0.61, 0.49, 0.0),
marble,
);
let tundra = Lerp::lerp(snow, Rgb::new(0.01, 0.3, 0.0), 0.4 + marble * 0.6);
let dead_tundra = Lerp::lerp(warm_stone, Rgb::new(0.3, 0.12, 0.2), marble);
let cliff = Rgb::lerp(cold_stone, warm_stone, marble);
let grass = Rgb::lerp(cold_grass, warm_grass, marble.powf(1.5));
let grass = Rgb::lerp(
cold_grass,
warm_grass,
marble.sub(0.5).add(1.0.sub(humidity).mul(0.5)).powf(1.5),
);
let snow_moss = Rgb::lerp(snow, cold_grass, 0.4 + marble.powf(1.5) * 0.6);
let moss = Rgb::lerp(dark_grass, cold_grass, marble.powf(1.5));
let rainforest = Rgb::lerp(wet_grass, warm_grass, marble.powf(1.5));
let sand = Rgb::lerp(beach_sand, desert_sand, marble);
let tropical = Rgb::lerp(
grass,
Rgb::lerp(
grass,
Rgb::new(0.15, 0.2, 0.15),
marble_small
.sub(0.5)
.mul(0.2)
.add(0.75.mul(1.0.sub(humidity)))
.powf(0.667),
),
Rgb::new(0.87, 0.62, 0.56),
marble_small.sub(0.5).mul(0.2).add(0.75).powf(0.667),
marble.powf(1.5).sub(0.5).mul(4.0),
);
// For below desert humidity, we are always sand or rock, depending on altitude and
// temperature.
let ground = Rgb::lerp(
Rgb::lerp(
snow,
grass,
dead_tundra,
sand,
temp.sub(CONFIG.snow_temp)
.sub((marble - 0.5) * 0.05)
.mul(256.0),
.div(CONFIG.desert_temp.sub(CONFIG.snow_temp))
.mul(0.5),
),
Rgb::lerp(tropical, sand, temp.sub(CONFIG.desert_temp).mul(32.0)),
temp.sub(CONFIG.tropical_temp).mul(16.0),
cliff,
alt.sub(CONFIG.mountain_scale * 0.25)
.div(CONFIG.mountain_scale * 0.125),
);
// From desert to forest humidity, we go from tundra to dirt to grass to moss to sand,
// depending on temperature.
let ground = Rgb::lerp(
ground,
Rgb::lerp(
Rgb::lerp(
Rgb::lerp(
Rgb::lerp(
tundra,
// snow_temp to 0
dirt,
temp.sub(CONFIG.snow_temp)
.div(CONFIG.snow_temp.neg())
/*.sub((marble - 0.5) * 0.05)
.mul(256.0)*/
.mul(1.0),
),
// 0 to tropical_temp
grass,
temp.div(CONFIG.tropical_temp).mul(4.0),
),
// tropical_temp to desert_temp
moss,
temp.sub(CONFIG.tropical_temp)
.div(CONFIG.desert_temp.sub(CONFIG.tropical_temp))
.mul(1.0),
),
// above desert_temp
sand,
temp.sub(CONFIG.desert_temp)
.div(1.0 - CONFIG.desert_temp)
.mul(4.0),
),
humidity
.sub(CONFIG.desert_hum)
.div(CONFIG.forest_hum.sub(CONFIG.desert_hum))
.mul(1.0),
);
// From forest to jungle humidity, we go from snow to dark grass to grass to tropics to sand
// depending on temperature.
let ground = Rgb::lerp(
ground,
Rgb::lerp(
Rgb::lerp(
Rgb::lerp(
snow_moss,
// 0 to tropical_temp
grass,
temp.div(CONFIG.tropical_temp).mul(4.0),
),
// tropical_temp to desert_temp
tropical,
temp.sub(CONFIG.tropical_temp)
.div(CONFIG.desert_temp.sub(CONFIG.tropical_temp))
.mul(1.0),
),
// above desert_temp
sand,
temp.sub(CONFIG.desert_temp)
.div(1.0 - CONFIG.desert_temp)
.mul(4.0),
),
humidity
.sub(CONFIG.forest_hum)
.div(CONFIG.jungle_hum.sub(CONFIG.forest_hum))
.mul(1.0),
);
// From jungle humidity upwards, we go from snow to grass to rainforest to tropics to sand.
let ground = Rgb::lerp(
ground,
Rgb::lerp(
Rgb::lerp(
Rgb::lerp(
snow_moss,
// 0 to tropical_temp
rainforest,
temp.div(CONFIG.tropical_temp).mul(4.0),
),
// tropical_temp to desert_temp
tropical,
temp.sub(CONFIG.tropical_temp)
.div(CONFIG.desert_temp.sub(CONFIG.tropical_temp))
.mul(4.0),
),
// above desert_temp
sand,
temp.sub(CONFIG.desert_temp)
.div(1.0 - CONFIG.desert_temp)
.mul(4.0),
),
humidity.sub(CONFIG.jungle_hum).mul(1.0),
);
// Snow covering
let ground = Rgb::lerp(
snow,
ground,
temp.sub(CONFIG.snow_temp)
.max(-humidity.sub(CONFIG.desert_hum))
.mul(16.0)
.add((marble_small - 0.5) * 0.5),
);
// Work out if we're on a path or near a town
@ -346,7 +484,7 @@ impl<'a> Sampler for ColumnGen<'a> {
/ 12.0,
),
(alt - CONFIG.sea_level - 0.25 * CONFIG.mountain_scale + marble * 128.0)
/ 100.0,
/ (0.25 * CONFIG.mountain_scale),
),
// Beach
((alt - CONFIG.sea_level - 1.0) / 2.0)

View File

@ -4,12 +4,18 @@ pub struct Config {
pub snow_temp: f32,
pub tropical_temp: f32,
pub desert_temp: f32,
pub desert_hum: f32,
pub forest_hum: f32,
pub jungle_hum: f32,
}
pub const CONFIG: Config = Config {
sea_level: 140.0,
mountain_scale: 1000.0,
snow_temp: -0.4,
tropical_temp: 0.25,
desert_temp: 0.45,
snow_temp: -0.6,
tropical_temp: 0.2,
desert_temp: 0.6,
desert_hum: 0.15,
forest_hum: 0.5,
jungle_hum: 0.85,
};

View File

@ -1,5 +1,10 @@
#![deny(unsafe_code)]
#![feature(euclidean_division, bind_by_move_pattern_guards, option_flattening)]
#![feature(
const_generics,
euclidean_division,
bind_by_move_pattern_guards,
option_flattening
)]
mod all;
mod block;

View File

@ -1,9 +1,11 @@
mod location;
mod settlement;
mod util;
// Reexports
pub use self::location::Location;
pub use self::settlement::Settlement;
use self::util::{cdf_irwin_hall, uniform_idx_as_vec2, uniform_noise, InverseCdf};
use crate::{
all::ForestKind,
@ -14,14 +16,45 @@ use common::{
terrain::{BiomeKind, TerrainChunkSize},
vol::VolSize,
};
use noise::{BasicMulti, HybridMulti, MultiFractal, NoiseFn, RidgedMulti, Seedable, SuperSimplex};
use noise::{
BasicMulti, Billow, HybridMulti, MultiFractal, NoiseFn, RidgedMulti, Seedable, SuperSimplex,
};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaChaRng;
use std::ops::{Add, Div, Mul, Neg, Sub};
use std::{
f32,
ops::{Add, Div, Mul, Neg, Sub},
};
use vek::*;
pub const WORLD_SIZE: Vec2<usize> = Vec2 { x: 1024, y: 1024 };
/// Calculates the smallest distance along an axis (x, y) from an edge of
/// the world. This value is maximal at WORLD_SIZE / 2 and minimized at the extremes
/// (0 or WORLD_SIZE on one or more axes). It then divides the quantity by cell_size,
/// so the final result is 1 when we are not in a cell along the edge of the world, and
/// ranges between 0 and 1 otherwise (lower when the chunk is closer to the edge).
fn map_edge_factor(posi: usize) -> f32 {
uniform_idx_as_vec2(posi)
.map2(WORLD_SIZE.map(|e| e as i32), |e, sz| {
(sz / 2 - (e - sz / 2).abs()) as f32 / 16.0
})
.reduce_partial_min()
.max(0.0)
.min(1.0)
}
/// A structure that holds cached noise values and cumulative distribution functions for the input
/// that led to those values. See the definition of InverseCdf for a description of how to
/// interpret the types of its fields.
struct GenCdf {
humid_base: InverseCdf,
temp_base: InverseCdf,
alt_base: InverseCdf,
chaos: InverseCdf,
alt: InverseCdf,
}
pub(crate) struct GenCtx {
pub turb_x_nz: SuperSimplex,
pub turb_y_nz: SuperSimplex,
@ -29,7 +62,11 @@ pub(crate) struct GenCtx {
pub alt_nz: HybridMulti,
pub hill_nz: SuperSimplex,
pub temp_nz: SuperSimplex,
// Fresh groundwater (currently has no effect, but should influence humidity)
pub dry_nz: BasicMulti,
// Humidity noise
pub humid_nz: Billow,
// Small amounts of noise for simulating rough terrain.
pub small_nz: BasicMulti,
pub rock_nz: HybridMulti,
pub cliff_nz: HybridMulti,
@ -86,13 +123,134 @@ impl WorldSim {
structure_gen: StructureGen2d::new(gen_seed(), 32, 24),
region_gen: StructureGen2d::new(gen_seed(), 400, 96),
cliff_gen: StructureGen2d::new(gen_seed(), 80, 56),
humid_nz: Billow::new()
.set_octaves(12)
.set_persistence(0.125)
.set_frequency(1.0)
// .set_octaves(6)
// .set_persistence(0.5)
.set_seed(gen_seed()),
};
// From 0 to 1.6, but the distribution before the max is from -1 and 1, so there is a 50%
// chance that hill will end up at 0.
let hill = uniform_noise(|_, wposf| {
(0.0 + gen_ctx
.hill_nz
.get((wposf.div(1_500.0)).into_array())
.mul(1.0) as f32
+ gen_ctx
.hill_nz
.get((wposf.div(400.0)).into_array())
.mul(0.3) as f32)
.add(0.3)
.max(0.0)
});
// 0 to 1, hopefully.
let humid_base = uniform_noise(|_, wposf| {
(gen_ctx.humid_nz.get(wposf.div(1024.0).into_array()) as f32)
.add(1.0)
.mul(0.5)
});
// -1 to 1.
let temp_base = uniform_noise(|_, wposf| {
(gen_ctx.temp_nz.get((wposf.div(12000.0)).into_array()) as f32)
});
// "Base" of the chunk, to be multiplied by CONFIG.mountain_scale (multiplied value is
// from -0.25 * (CONFIG.mountain_scale * 1.1) to 0.25 * (CONFIG.mountain_scale * 0.9),
// but value here is from -0.275 to 0.225).
let alt_base = uniform_noise(|_, wposf| {
(gen_ctx.alt_nz.get((wposf.div(12_000.0)).into_array()) as f32)
.sub(0.1)
.mul(0.25)
});
// chaos produces a value in [0.1, 1.24]. It is a meta-level factor intended to reflect how
// "chaotic" the region is--how much weird stuff is going on on this terrain.
let chaos = uniform_noise(|posi, wposf| {
(gen_ctx.chaos_nz.get((wposf.div(3_000.0)).into_array()) as f32)
.add(1.0)
.mul(0.5)
// [0, 1] * [0.25, 1] = [0, 1] (but probably towards the lower end)
.mul(
(gen_ctx.chaos_nz.get((wposf.div(6_000.0)).into_array()) as f32)
.abs()
.max(0.25)
.min(1.0),
)
// Chaos is always increased by a little when we're on a hill (but remember that
// hill is 0 about 50% of the time).
// [0, 1] + 0.15 * [0, 1.6] = [0, 1.24]
.add(0.2 * hill[posi].1)
// [0, 1.24] * [0.35, 1.0] = [0, 1.24].
// Sharply decreases (towards 0.35) when temperature is near desert_temp (from below),
// then saturates just before it actually becomes desert. Otherwise stays at 1.
// Note that this is not the *final* temperature, only the initial noise value for
// temperature.
.mul(
temp_base[posi]
.1
.sub(0.45)
.neg()
.mul(12.0)
.max(0.35)
.min(1.0),
)
// We can't have *no* chaos!
.max(0.1)
});
// We ignore sea level because we actually want to be relative to sea level here and want
// things in CONFIG.mountain_scale units, but otherwise this is a correct altitude
// calculation. Note that this is using the "unadjusted" temperature.
let alt = uniform_noise(|posi, wposf| {
// This is the extension upwards from the base added to some extra noise from -1 to 1.
// The extra noise is multiplied by alt_main (the mountain part of the extension)
// clamped to [0.25, 1], and made 60% larger (so the extra noise is between [-1.6, 1.6],
// and the final noise is never more than 160% or less than 40% of the original noise,
// depending on altitude).
// Adding this to alt_main thus yields a value between -0.4 (if alt_main = 0 and
// gen_ctx = -1) and 2.6 (if alt_main = 1 and gen_ctx = 1). When the generated small_nz
// value hits -0.625 the value crosses 0, so most of the points are above 0.
//
// Then, we add 1 and divide by 2 to get a value between 0.3 and 1.8.
let alt_main = {
// Extension upwards from the base. A positive number from 0 to 1 curved to be
// maximal at 0. Also to be multiplied by CONFIG.mountain_scale.
let alt_main = (gen_ctx.alt_nz.get((wposf.div(2_000.0)).into_array()) as f32)
.abs()
.powf(1.45);
(0.0 + alt_main
+ (gen_ctx.small_nz.get((wposf.div(300.0)).into_array()) as f32)
.mul(alt_main.max(0.25))
.mul(0.3))
.add(1.0)
.mul(0.5)
};
// Now we can compute the final altitude using chaos.
// We multiply by chaos clamped to [0.1, 1.24] to get a value between 0.03 and 2.232 for
// alt_pre, then multiply by CONFIG.mountain_scale and add to the base and sea level to
// get an adjusted value, then multiply the whole thing by map_edge_factor
// (TODO: compute final bounds).
(alt_base[posi].1 + alt_main.mul(chaos[posi].1)).mul(map_edge_factor(posi))
});
let gen_cdf = GenCdf {
humid_base,
temp_base,
alt_base,
chaos,
alt,
};
let mut chunks = Vec::new();
for x in 0..WORLD_SIZE.x as i32 {
for y in 0..WORLD_SIZE.y as i32 {
chunks.push(SimChunk::generate(Vec2::new(x, y), &mut gen_ctx));
}
for i in 0..WORLD_SIZE.x * WORLD_SIZE.y {
chunks.push(SimChunk::generate(i, &mut gen_ctx, &gen_cdf));
}
let mut this = Self {
@ -304,6 +462,7 @@ pub struct SimChunk {
pub alt: f32,
pub temp: f32,
pub dryness: f32,
pub humidity: f32,
pub rockiness: f32,
pub is_cliffs: bool,
pub near_cliffs: bool,
@ -328,23 +487,13 @@ pub struct LocationInfo {
}
impl SimChunk {
fn generate(pos: Vec2<i32>, gen_ctx: &mut GenCtx) -> Self {
fn generate(posi: usize, gen_ctx: &mut GenCtx, gen_cdf: &GenCdf) -> Self {
let pos = uniform_idx_as_vec2(posi);
let wposf = (pos * TerrainChunkSize::SIZE.map(|e| e as i32)).map(|e| e as f64);
let hill = (0.0
+ gen_ctx
.hill_nz
.get((wposf.div(1_500.0)).into_array())
.mul(1.0) as f32
+ gen_ctx
.hill_nz
.get((wposf.div(500.0)).into_array())
.mul(0.3) as f32)
.add(0.3)
.max(0.0);
let temp = gen_ctx.temp_nz.get((wposf.div(12000.0)).into_array()) as f32;
// FIXME: Currently unused, but should represent fresh groundwater level.
// Should be correlated a little with humidity, somewhat negatively with altitude,
// and very negatively with difference in temperature from zero.
let dryness = gen_ctx.dry_nz.get(
(wposf
.add(Vec2::new(
@ -358,62 +507,76 @@ impl SimChunk {
.into_array(),
) as f32;
let chaos = (gen_ctx.chaos_nz.get((wposf.div(3_000.0)).into_array()) as f32)
.add(1.0)
.mul(0.5)
.mul(
(gen_ctx.chaos_nz.get((wposf.div(6_000.0)).into_array()) as f32)
.abs()
.max(0.25)
.min(1.0),
)
.add(0.15 * hill)
.mul(
temp.sub(CONFIG.desert_temp)
.neg()
.mul(12.0)
.max(0.35)
.min(1.0),
)
.max(0.1);
let (_, alt_base) = gen_cdf.alt_base[posi];
let map_edge_factor = map_edge_factor(posi);
let (_, chaos) = gen_cdf.chaos[posi];
let (humid_uniform, _) = gen_cdf.humid_base[posi];
let (alt_uniform, alt_pre) = gen_cdf.alt[posi];
let (temp_uniform, _) = gen_cdf.temp_base[posi];
let alt_base = (gen_ctx.alt_nz.get((wposf.div(12_000.0)).into_array()) as f32)
.mul(250.0)
.sub(25.0);
// Take the weighted average of our randomly generated base humidity, the scaled
// negative altitude, and other random variable (to add some noise) to yield the
// final humidity. Note that we are using the "old" version of chaos here.
const HUMID_WEIGHTS: [f32; 2] = [1.0, 1.0];
let humidity = cdf_irwin_hall(&HUMID_WEIGHTS, [humid_uniform, 1.0 - alt_uniform]);
let alt_main = (gen_ctx.alt_nz.get((wposf.div(2_000.0)).into_array()) as f32)
.abs()
.powf(1.35);
// We also correlate temperature negatively with altitude using different weighting than we
// use for humidity.
const TEMP_WEIGHTS: [f32; 2] = [2.0, 1.0];
let temp = cdf_irwin_hall(&TEMP_WEIGHTS, [temp_uniform, 1.0 - alt_uniform])
// Convert to [-1, 1]
.sub(0.5)
.mul(2.0);
let map_edge_factor = pos
.map2(WORLD_SIZE.map(|e| e as i32), |e, sz| {
(sz / 2 - (e - sz / 2).abs()) as f32 / 16.0
})
.reduce_partial_min()
.max(0.0)
.min(1.0);
let alt = (CONFIG.sea_level
+ alt_base
+ (0.0
+ alt_main
+ (gen_ctx.small_nz.get((wposf.div(300.0)).into_array()) as f32)
.mul(alt_main.max(0.25))
.mul(1.6))
.add(1.0)
.mul(0.5)
.mul(chaos)
.mul(CONFIG.mountain_scale))
* map_edge_factor;
let alt_base = alt_base.mul(CONFIG.mountain_scale);
let alt = CONFIG
.sea_level
.mul(map_edge_factor)
.add(alt_pre.mul(CONFIG.mountain_scale));
let cliff = gen_ctx.cliff_nz.get((wposf.div(2048.0)).into_array()) as f32 + chaos * 0.2;
// Logistic regression. Make sure x ∈ (0, 1).
let logit = |x: f32| x.ln() - x.neg().ln_1p();
// 0.5 + 0.5 * tanh(ln(1 / (1 - 0.1) - 1) / (2 * (sqrt(3)/pi)))
let logistic_2_base = 3.0f32.sqrt().mul(f32::consts::FRAC_2_PI);
// Assumes μ = 0, σ = 1
let logistic_cdf = |x: f32| x.div(logistic_2_base).tanh().mul(0.5).add(0.5);
// No trees in the ocean or with zero humidity (currently)
let tree_density = if alt <= CONFIG.sea_level + 5.0 {
0.0
} else {
let tree_density = (gen_ctx.tree_nz.get((wposf.div(1024.0)).into_array()) as f32)
.mul(1.5)
.add(1.0)
.mul(0.5)
.mul(1.2 - chaos * 0.95)
.add(0.05)
.max(0.0)
.min(1.0);
// Tree density should go (by a lot) with humidity.
if humidity <= 0.0 || tree_density <= 0.0 {
0.0
} else if humidity >= 1.0 || tree_density >= 1.0 {
1.0
} else {
// Weighted logit sum.
logistic_cdf(logit(humidity) + 0.5 * logit(tree_density))
}
// rescale to (-0.9, 0.9)
.sub(0.5)
.mul(0.9)
.add(0.5)
};
Self {
chaos,
alt_base,
alt,
temp,
dryness,
humidity,
rockiness: (gen_ctx.rock_nz.get((wposf.div(1024.0)).into_array()) as f32)
.sub(0.1)
.mul(1.3)
@ -423,31 +586,63 @@ impl SimChunk {
&& alt > CONFIG.sea_level + 5.0
&& dryness.abs() > 0.075,
near_cliffs: cliff > 0.25,
tree_density: (gen_ctx.tree_nz.get((wposf.div(1024.0)).into_array()) as f32)
.mul(1.5)
.add(1.0)
.mul(0.5)
.mul(1.2 - chaos * 0.95)
.add(0.05)
.mul(if alt > CONFIG.sea_level + 5.0 {
1.0
} else {
0.0
})
.max(0.0),
tree_density,
forest_kind: if temp > 0.0 {
if temp > CONFIG.desert_temp {
ForestKind::Palm
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 {
ForestKind::Savannah
if humidity > CONFIG.jungle_hum {
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 {
ForestKind::Oak
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 treet 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 {
if temp > CONFIG.snow_temp {
// 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 && humidity > CONFIG.forest_hum {
ForestKind::SnowPine
} else if humidity > CONFIG.desert_hum {
ForestKind::Pine
} else {
ForestKind::SnowPine
// Should really have something like tundra.
ForestKind::Pine
}
},
spawn_rate: 1.0,

160
world/src/sim/util.rs Normal file
View File

@ -0,0 +1,160 @@
use super::WORLD_SIZE;
use common::{terrain::TerrainChunkSize, vol::VolSize};
use vek::*;
/// Computes the cumulative distribution function of the weighted sum of k independent,
/// uniformly distributed random variables between 0 and 1. For each variable i, we use weights[i]
/// as the weight to give samples[i] (the weights should all be positive).
///
/// If the precondition is met, the distribution of the result of calling this function will be
/// uniformly distributed while preserving the same information that was in the original average.
///
/// For N > 33 the function will no longer return correct results since we will overflow u32.
///
/// NOTE:
///
/// Per [1], the problem of determing the CDF of
/// the sum of uniformly distributed random variables over *different* ranges is considerably more
/// complicated than it is for the same-range case. Fortunately, it also provides a reference to
/// [2], which contains a complete derivation of an exact rule for the density function for
/// this case. The CDF is just the integral of the cumulative distribution function [3],
/// which we use to convert this into a CDF formula.
///
/// This allows us to sum weighted, uniform, independent random variables.
///
/// At some point, we should probably contribute this back to stats-rs.
///
/// 1. https://www.r-bloggers.com/sums-of-random-variables/,
/// 2. Sadooghi-Alvandi, S., A. Nematollahi, & R. Habibi, 2009.
/// On the Distribution of the Sum of Independent Uniform Random Variables.
/// Statistical Papers, 50, 171-175.
/// 3. hhttps://en.wikipedia.org/wiki/Cumulative_distribution_function
pub fn cdf_irwin_hall<const N: usize>(weights: &[f32; N], samples: [f32; N]) -> f32 {
// Let J_k = {(j_1, ... , j_k) : 1 ≤ j_1 < j_2 < ··· < j_k ≤ N }.
//
// Let A_N = Π{k = 1 to n}a_k.
//
// The density function for N ≥ 2 is:
//
// 1/(A_N * (N - 1)!) * (x^(N-1) + Σ{k = 1 to N}((-1)^k *
// Σ{(j_1, ..., j_k) ∈ J_k}(max(0, x - Σ{l = 1 to k}(a_(j_l)))^(N - 1))))
//
// So the cumulative distribution function is its integral, i.e. (I think)
//
// 1/(product{k in A}(k) * N!) * (x^N + sum(k in 1 to N)((-1)^k *
// sum{j in Subsets[A, {k}]}(max(0, x - sum{l in j}(l))^N)))
//
// which is also equivalent to
//
// (letting B_k = { a in Subsets[A, {k}] : sum {l in a} l }, B_(0,1) = 0 and
// H_k = { i : 1 ≤ 1 ≤ N! / (k! * (N - k)!) })
//
// 1/(product{k in A}(k) * N!) * sum(k in 0 to N)((-1)^k *
// sum{l in H_k}(max(0, x - B_(k,l))^N))
//
// We should be able to iterate through the whole power set
// instead, and figure out K by calling count_ones(), so we can compute the result in O(2^N)
// iterations.
let x: f32 = weights
.iter()
.zip(samples.iter())
.map(|(weight, sample)| weight * sample)
.sum();
let mut y = 0.0f32;
for subset in 0u32..(1 << N) {
// Number of set elements
let k = subset.count_ones();
// Add together exactly the set elements to get B_subset
let z = weights
.iter()
.enumerate()
.filter(|(i, _)| subset & (1 << i) as u32 != 0)
.map(|(_, k)| k)
.sum::<f32>();
// Compute max(0, x - B_subset)^N
let z = (x - z).max(0.0).powi(N as i32);
// The parity of k determines whether the sum is negated.
y += if k & 1 == 0 { z } else { -z };
}
// Divide by the product of the weights.
y /= weights.iter().product::<f32>();
// Remember to multiply by 1 / N! at the end.
y / (1..=N as i32).product::<i32>() as f32
}
/// First component of each element of the vector is the computed CDF of the noise function at this
/// index (i.e. its position in a sorted list of value returned by the noise function applied to
/// every chunk in the game). Second component is the cached value of the noise function that
/// generated the index.
///
/// NOTE: Length should always be WORLD_SIZE.x * WORLD_SIZE.y.
pub type InverseCdf = Box<[(f32, f32)]>;
/// Computes the position Vec2 of a SimChunk from an index, where the index was generated by
/// uniform_noise.
pub fn uniform_idx_as_vec2(idx: usize) -> Vec2<i32> {
Vec2::new((idx / WORLD_SIZE.x) as i32, (idx % WORLD_SIZE.x) as i32)
}
/// Compute inverse cumulative distribution function for arbitrary function f, the hard way. We
/// pre-generate noise values prior to worldgen, then sort them in order to determine the correct
/// position in the sorted order. That lets us use `(index + 1) / (WORLDSIZE.y * WORLDSIZE.x)` as
/// a uniformly distributed (from almost-0 to 1) regularization of the chunks. That is, if we
/// apply the computed "function" F⁻¹(x, y) to (x, y) and get out p, it means that approximately
/// (100 * p)% of chunks have a lower value for F⁻¹ than p. The main purpose of doing this is to
/// make sure we are using the entire range we want, and to allow us to apply the numerous results
/// about distributions on uniform functions to the procedural noise we generate, which lets us
/// much more reliably control the *number* of features in the world while still letting us play
/// with the *shape* of those features, without having arbitrary cutoff points / discontinuities
/// (which tend to produce ugly-looking / unnatural terrain).
///
/// As a concrete example, before doing this it was very hard to tweak humidity so that either most
/// of the world wasn't dry, or most of it wasn't wet, by combining the billow noise function and
/// the computed altitude. This is because the billow noise function has a very unusual
/// distribution that is heavily skewed towards 0. By correcting for this tendency, we can start
/// with uniformly distributed billow noise and altitudes and combine them to get uniformly
/// distributed humidity, while still preserving the existing shapes that the billow noise and
/// altitude functions produce.
///
/// f takes an index, which represents the index corresponding to this chunk in any any SimChunk
/// vector returned by uniform_noise, and (for convenience) the float-translated version of those
/// coordinates.
/// f should return a value with no NaNs. If there is a NaN, it will panic. There are no other
/// conditions on f.
///
/// Returns a vec of (f32, f32) pairs consisting of the percentage of chunks with a value lower than
/// this one, and the actual noise value (we don't need to cache it, but it makes ensuring that
/// subsequent code that needs the noise value actually uses the same one we were using here
/// easier).
pub fn uniform_noise(f: impl Fn(usize, Vec2<f64>) -> f32) -> InverseCdf {
let mut noise = (0..WORLD_SIZE.x * WORLD_SIZE.y)
.map(|i| {
(
i,
f(
i,
(uniform_idx_as_vec2(i) * TerrainChunkSize::SIZE.map(|e| e as i32))
.map(|e| e as f64),
),
)
})
.collect::<Vec<_>>();
// sort_unstable_by is equivalent to sort_by here since we include the index in the
// comparison. We could leave out the index, but this might make the order not
// reproduce the same way between different versions of Rust (for example).
noise.sort_unstable_by(|f, g| (f.1, f.0).partial_cmp(&(g.1, g.0)).unwrap());
// Construct a vector that associates each chunk position with the 1-indexed
// position of the noise in the sorted vector (divided by the vector length).
// This guarantees a uniform distribution among the samples.
let mut uniform_noise = vec![(0.0, 0.0); WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice();
let total = (WORLD_SIZE.x * WORLD_SIZE.y) as f32;
for (noise_idx, (chunk_idx, noise_val)) in noise.into_iter().enumerate() {
uniform_noise[chunk_idx] = ((1 + noise_idx) as f32 / total, noise_val);
}
uniform_noise
}