diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 0dd727cfb9..d6ffd80a0c 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -76,8 +76,11 @@ impl<'a> ColumnGen<'a> { }); let chunk = self.world.sim().get(chunk_pos)?; - if seed % 5 == 2 && chunk.temp > CONFIG.desert_temp && chunk.humidity < CONFIG.desert_hum && - 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, @@ -203,8 +206,11 @@ 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.5); + let humidity = humidity.add((marble - 0.5) * 0.5); + // 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); @@ -212,44 +218,39 @@ impl<'a> Sampler for ColumnGen<'a> { 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), - marble, - ); - let dead_tundra = Lerp::lerp( - warm_stone, - Rgb::new(0.35, 0.05, 0.2), - 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.sub(0.5).add(1.0.sub(humidity).mul(0.5)).powf(1.5) + marble.sub(0.5).add(1.0.sub(humidity).mul(0.5)).powf(1.5), ); - let snow_moss = Rgb::lerp(snow, cold_grass, marble.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( 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) + 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.powf(1.5).sub(0.5).mul(4.0) + marble.powf(1.5).sub(0.5).mul(4.0), ); // For below desert humidity, we are always sand or rock, depending on altitude and @@ -260,10 +261,11 @@ impl<'a> Sampler for ColumnGen<'a> { sand, temp.sub(CONFIG.snow_temp) .div(CONFIG.desert_temp.sub(CONFIG.snow_temp)) - .mul(0.5) + .mul(0.5), ), cliff, - alt.sub(CONFIG.mountain_scale * 0.25).div(CONFIG.mountain_scale * 0.125) + 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. @@ -273,41 +275,35 @@ impl<'a> Sampler for ColumnGen<'a> { Rgb::lerp( Rgb::lerp( Rgb::lerp( - // below snow_temp - Rgb::lerp( - tundra, - snow, - humidity.sub(CONFIG.desert_hum) - .sub((marble - 0.5) * 0.05) - .mul(256.0) - ), + 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) + .mul(1.0), ), // 0 to tropical_temp grass, - temp.div(CONFIG.tropical_temp).mul(4.0) + 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) + .mul(1.0), ), // above desert_temp sand, temp.sub(CONFIG.desert_temp) .div(1.0 - CONFIG.desert_temp) - .mul(4.0) + .mul(4.0), ), - humidity.sub(CONFIG.desert_hum) - .div(CONFIG.forest_hum.sub(CONFIG.desert_hum)) - .mul(1.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. @@ -316,24 +312,16 @@ impl<'a> Sampler for ColumnGen<'a> { Rgb::lerp( Rgb::lerp( Rgb::lerp( - Rgb::lerp( - // below snow_temp - snow, - // snow_temp to 0 - snow_moss, - temp.sub(CONFIG.snow_temp)/*.div(CONFIG.snow_temp.neg())*/ - .sub((marble - 0.5) * 0.05) - .mul(256.0) - ), + snow_moss, // 0 to tropical_temp grass, - temp.div(CONFIG.tropical_temp).mul(4.0) + 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) + .mul(1.0), ), // above desert_temp sand, @@ -341,9 +329,10 @@ impl<'a> Sampler for ColumnGen<'a> { .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) + 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( @@ -351,24 +340,16 @@ impl<'a> Sampler for ColumnGen<'a> { Rgb::lerp( Rgb::lerp( Rgb::lerp( - Rgb::lerp( - // below snow_temp - snow, - // snow_temp to 0 - snow_moss, - temp.sub(CONFIG.snow_temp)/*.div(CONFIG.snow_temp.neg())*/ - .sub((marble - 0.5) * 0.05) - .mul(256.0) - ), + snow_moss, // 0 to tropical_temp rainforest, - temp.div(CONFIG.tropical_temp).mul(4.0) + 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) + .mul(4.0), ), // above desert_temp sand, @@ -376,7 +357,17 @@ impl<'a> Sampler for ColumnGen<'a> { .div(1.0 - CONFIG.desert_temp) .mul(4.0), ), - humidity.sub(CONFIG.jungle_hum).mul(1.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) * 2.0), ); // Work out if we're on a path or near a town diff --git a/world/src/lib.rs b/world/src/lib.rs index 2ebf0edffa..65a0283e75 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -1,9 +1,10 @@ #![deny(unsafe_code)] -#![feature(box_syntax, - const_generics, - euclidean_division, - bind_by_move_pattern_guards, - option_flattening, +#![feature( + box_syntax, + const_generics, + euclidean_division, + bind_by_move_pattern_guards, + option_flattening )] mod all; diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 896c7365b3..d775b0a0df 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -14,7 +14,9 @@ use common::{ terrain::{BiomeKind, TerrainChunkSize}, vol::VolSize, }; -use noise::{BasicMulti, Billow, 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::{ @@ -52,7 +54,7 @@ pub const WORLD_SIZE: Vec2<usize> = Vec2 { x: 1024, y: 1024 }; /// 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 -fn cdf_irwin_hall<const N : usize>(weights: &[f32; N], samples: [f32; N]) -> f32 { +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. @@ -78,17 +80,21 @@ fn cdf_irwin_hall<const N : usize>(weights: &[f32; N], samples: [f32; N]) -> f32 // 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 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() + let z = weights + .iter() .enumerate() - .filter( |(i, _)| subset & (1 << i) as u32 != 0) + .filter(|(i, _)| subset & (1 << i) as u32 != 0) .map(|(_, k)| k) .sum::<f32>(); // Compute max(0, x - B_subset)^N @@ -148,11 +154,16 @@ fn uniform_idx_as_vec2(idx: usize) -> Vec2<i32> { /// easier). 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)) - )) + .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 @@ -205,7 +216,7 @@ pub(crate) struct GenCtx { // Fresh groundwater (currently has no effect, but should influence humidity) pub dry_nz: BasicMulti, // Humidity noise - pub humid_nz : Billow, + pub humid_nz: Billow, // Small amounts of noise for simulating rough terrain. pub small_nz: BasicMulti, pub rock_nz: HybridMulti, @@ -274,66 +285,72 @@ impl WorldSim { // 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 + 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(500.0)).into_array()) - .mul(0.3) as f32) - .add(0.3) - .max(0.0)); + + gen_ctx + .hill_nz + .get((wposf.div(500.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)); + 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) - ); + 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)); + 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.15 * 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. - .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)); + 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. + .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) + }); // 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 @@ -350,13 +367,12 @@ impl WorldSim { // 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.35); + .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.16)) + (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.2)) .add(1.0) .mul(0.5) }); @@ -364,9 +380,10 @@ impl WorldSim { // We ignore sea level because we actually want to be relative to sea level here and want // things in CONFIG.mountain_scale units, and we are using the version of chaos that doesn't // know about temperature. Otherwise, this is a correct altitude calculation. - let alt_pre = uniform_noise(|posi, _| + let alt_pre = uniform_noise(|posi, _| { (alt_base[posi].1 + alt_main[posi].1.mul(chaos[posi].1.max(0.1))) - .mul(map_edge_factor(posi))); + .mul(map_edge_factor(posi)) + }); let gen_cdf = GenCdf { humid_base, @@ -646,23 +663,15 @@ impl SimChunk { // 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_base, - 1.0 - alt_uniform, - ]); + const HUMID_WEIGHTS: [f32; 2] = [1.0, 1.0]; + let humidity = cdf_irwin_hall(&HUMID_WEIGHTS, [humid_base, 1.0 - alt_uniform]); let (temp_base, temp_old) = gen_cdf.temp_base[posi]; // 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_base, - 1.0 - alt_uniform, - ]) + let temp = cdf_irwin_hall(&TEMP_WEIGHTS, [temp_base, 1.0 - alt_uniform]) // Convert to [-1, 1] .sub(0.5) .mul(2.0); @@ -672,8 +681,8 @@ impl SimChunk { // 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). let alt_base = alt_base.mul(CONFIG.mountain_scale); - let alt = - CONFIG.sea_level + let alt = CONFIG + .sea_level .add(alt_base) .add(alt_pre.mul(chaos).mul(CONFIG.mountain_scale)) .mul(map_edge_factor); @@ -681,7 +690,7 @@ impl SimChunk { 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(); + 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 @@ -690,7 +699,9 @@ impl SimChunk { let f = |humidity, density| logistic_cdf(logit(humidity) + 0.5 * logit(density)); // 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 = 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) @@ -707,10 +718,10 @@ impl SimChunk { } else { logistic_cdf(logit(humidity) + 0.5 * logit(tree_density)) } - // rescale to (-0.9, 0.9) - .sub(0.5) - .mul(0.9) - .add(0.5) + // rescale to (-0.9, 0.9) + .sub(0.5) + .mul(0.9) + .add(0.5) }; Self { @@ -737,7 +748,7 @@ impl SimChunk { // should probably be different from palm trees, but we use them // for now. ForestKind::Palm - } else if humidity > CONFIG.forest_hum { + } 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