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 8ec64ff148
12 changed files with 639 additions and 103 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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
}