From 13b6d4d534cc4814b7cb3294ca41bbfea0a6b186 Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Tue, 28 Jul 2020 10:55:48 +0200 Subject: [PATCH] Removing WORLD_SIZE, part 1. Erased almost every instance of WORLD_SIZE and replaced it with a local power of two, map_size_lg (which respects certain invariants; see common/src/terrain/map.rs for more details about MapSizeLg). This also means we can avoid a dependency on the world crate from client, as desired. Now that the rest of the code is not expecting a fixed WORLD_SIZE, the next step is to arrange for maps to store their world size, and to use that world size as a basis prior to loading the map (as well, probably, as prior to configuring some of the noise functions). --- Cargo.lock | 3 +- client/Cargo.toml | 1 - client/src/lib.rs | 49 +- common/Cargo.toml | 1 + common/src/lib.rs | 2 + common/src/msg/server.rs | 8 +- common/src/terrain/map.rs | 719 +++++++++++++++++++++++++++++ common/src/terrain/mod.rs | 158 ++++++- server/src/lib.rs | 6 +- server/src/test_world.rs | 12 +- world/Cargo.toml | 1 - world/examples/water.rs | 101 +++-- world/src/civ/mod.rs | 16 +- world/src/column/mod.rs | 115 +---- world/src/lib.rs | 11 +- world/src/sim/erosion.rs | 227 +++++---- world/src/sim/map.rs | 934 ++++++++++---------------------------- world/src/sim/mod.rs | 236 ++++++---- world/src/sim/util.rs | 142 +++--- 19 files changed, 1593 insertions(+), 1149 deletions(-) create mode 100644 common/src/terrain/map.rs diff --git a/Cargo.lock b/Cargo.lock index ec1bb05f42..2349fcbc16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4565,7 +4565,6 @@ dependencies = [ "uvth 3.1.1", "vek", "veloren-common", - "veloren-world", "veloren_network", ] @@ -4588,6 +4587,7 @@ dependencies = [ "rand 0.7.3", "rayon", "ron", + "roots", "serde", "serde_json", "specs", @@ -4733,7 +4733,6 @@ dependencies = [ "rand_chacha 0.2.2", "rayon", "ron", - "roots", "serde", "tracing", "tracing-subscriber", diff --git a/client/Cargo.toml b/client/Cargo.toml index c1b0d61c6f..754c72cde3 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,7 +6,6 @@ edition = "2018" [dependencies] common = { package = "veloren-common", path = "../common", features = ["no-assets"] } -world = { package = "veloren-world", path = "../world" } network = { package = "veloren_network", path = "../network", default-features = false } byteorder = "1.3.2" diff --git a/client/src/lib.rs b/client/src/lib.rs index 8138288872..3222c7e51c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -28,7 +28,7 @@ use common::{ recipe::RecipeBook, state::State, sync::{Uid, UidAllocator, WorldSyncExt}, - terrain::{block::Block, TerrainChunk, TerrainChunkSize}, + terrain::{block::Block, neighbors, TerrainChunk, TerrainChunkSize}, vol::RectVolSize, }; use futures_executor::block_on; @@ -50,9 +50,6 @@ use std::{ use tracing::{debug, error, trace, warn}; use uvth::{ThreadPool, ThreadPoolBuilder}; use vek::*; -// TODO: remove world dependencies. We should see if we -// can pull out map drawing into common somehow. -use world::sim::{neighbors, Alt}; // The duration of network inactivity until the player is kicked // @TODO: in the future, this should be configurable on the server @@ -187,8 +184,18 @@ impl Client { let entity = state.ecs_mut().apply_entity_package(entity_package); *state.ecs_mut().write_resource() = time_of_day; - let map_size = world_map.dimensions; + let map_size_lg = common::terrain::MapSizeLg::new( + world_map.dimensions_lg, + ) + .map_err(|_| { + Error::Other(format!( + "Server sent bad world map dimensions: {:?}", + world_map.dimensions_lg, + )) + })?; + let map_size = map_size_lg.chunks(); let max_height = world_map.max_height; + let sea_level = world_map.sea_level; let rgba = world_map.rgba; let alt = world_map.alt; let expected_size = @@ -203,10 +210,9 @@ impl Client { } let [west, east] = world_map.horizons; let scale_angle = - |a: u8| (a as Alt / 255.0 * ::FRAC_PI_2()).tan(); - let scale_height = |h: u8| h as Alt / 255.0 * max_height as Alt; - let scale_height_big = - |h: u32| (h >> 3) as Alt / 8191.0 * max_height as Alt; + |a: u8| (a as f32 / 255.0 * ::FRAC_PI_2()).tan(); + let scale_height = |h: u8| h as f32 / 255.0 * max_height; + let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height; debug!("Preparing image..."); let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| { @@ -223,13 +229,15 @@ impl Client { // Redraw map (with shadows this time). let mut world_map = vec![0u32; rgba.len()]; - let mut map_config = world::sim::MapConfig::default(); - map_config.lgain = 1.0; - map_config.gain = max_height; + let mut map_config = common::terrain::map::MapConfig::orthographic( + map_size_lg, + core::ops::RangeInclusive::new(0.0, max_height), + ); + // map_config.gain = max_height; map_config.horizons = Some(&horizons); // map_config.light_direction = Vec3::new(1.0, -1.0, 0.0); - map_config.focus.z = 0.0; - let rescale_height = |h: Alt| (h / max_height as Alt) as f32; + // map_config.focus.z = 0.0; + let rescale_height = |h: f32| h / max_height; let bounds_check = |pos: Vec2| { pos.reduce_partial_min() >= 0 && pos.x < map_size.x as i32 @@ -246,9 +254,7 @@ impl Client { let downhill = { let mut best = -1; let mut besth = alti; - // TODO: Fix to work for dynamic WORLD_SIZE (i.e. - // map_size). - for nposi in neighbors(posi) { + for nposi in neighbors(map_size_lg, posi) { let nbh = alt[nposi]; if nbh < besth { besth = nbh; @@ -278,9 +284,9 @@ impl Client { wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32), ); let alt = rescale_height(scale_height_big(alt)); - world::sim::MapSample { + common::terrain::map::MapSample { rgb: Rgb::from(rgba), - alt: alt as Alt, + alt: f64::from(alt), downhill_wpos, connections: None, } @@ -326,10 +332,7 @@ impl Client { .collect::>(); let lod_horizon = horizons; //make_raw(&horizons)?; // TODO: Get sea_level from server. - let map_bounds = Vec2::new( - /* map_config.focus.z */ world::CONFIG.sea_level, - /* map_config.gain */ max_height, - ); + let map_bounds = Vec2::new(sea_level, max_height); debug!("Done preparing image..."); break Ok(( diff --git a/common/Cargo.toml b/common/Cargo.toml index 47dd092a67..51c2ce83d6 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -11,6 +11,7 @@ no-assets = [] arraygen = "0.1.13" specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", branch = "specs-git" } +roots = "0.0.5" specs = { git = "https://github.com/amethyst/specs.git", features = ["serde", "storage-event-control"], rev = "7a2e348ab2223818bad487695c66c43db88050a5" } vek = { version = "0.11.0", features = ["serde"] } dot_vox = "4.0" diff --git a/common/src/lib.rs b/common/src/lib.rs index fb04ff28b1..628e562400 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,6 +3,8 @@ #![type_length_limit = "1664759"] #![feature( arbitrary_enum_discriminant, + const_checked_int_methods, + const_if_match, option_unwrap_none, bool_to_option, label_break_value, diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index eb0aafe534..aaf0bd3477 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -66,8 +66,12 @@ pub struct CharacterInfo { /// repeating the "small angles" optimization that works well on more detailed /// shadow maps intended for height maps. pub struct WorldMapMsg { - /// World map dimensions (width × height) - pub dimensions: Vec2, + /// Log base 2 of world map dimensions (width × height) in chunks. + /// + /// NOTE: Invariant: chunk count fits in a u16. + pub dimensions_lg: Vec2, + /// Sea level (used to provide a base altitude). + pub sea_level: f32, /// Max height (used to scale altitudes). pub max_height: f32, /// RGB+A; the alpha channel is currently unused, but will be used in the diff --git a/common/src/terrain/map.rs b/common/src/terrain/map.rs new file mode 100644 index 0000000000..573fee914c --- /dev/null +++ b/common/src/terrain/map.rs @@ -0,0 +1,719 @@ +use super::{ + neighbors, quadratic_nearest_point, river_spline_coeffs, uniform_idx_as_vec2, + vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA, TERRAIN_CHUNK_BLOCKS_LG, +}; +use crate::vol::RectVolSize; +use core::{f32, f64, iter, ops::RangeInclusive}; +use vek::*; + +/// Base two logarithm of the maximum size of the precomputed world, in meters, +/// along the x (E/W) and y (N/S) dimensions. +/// +/// NOTE: Each dimension is guaranteed to be a power of 2, so the logarithm is +/// exact. This is so that it is possible (at least in theory) for compiler or +/// runtime optimizations exploiting this are possible. For example, division +/// by the chunk size can turn into a bit shift. +/// +/// NOTE: As an invariant, this value is at least [TERRAIN_CHUNK_BLOCKS_LG]. +/// +/// NOTE: As an invariant, `(1 << [MAX_WORLD_BLOCKS_LG])` fits in an i32. +/// +/// TODO: Add static assertions for the above invariants. +/// +/// Currently, we define the maximum to be 19 (corresponding to 2^19 m) for both +/// directions. This value was derived by backwards reasoning from the following +/// conservative estimate of the maximum landmass area (using an approximation +/// of 1024 blocks / km instead of 1000 blocks / km, which will result in an +/// estimate that is strictly lower than the real landmass): +/// +/// Max area (km²) +/// ≌ (2^19 blocks * 1 km / 1024 blocks)^2 +/// = 2^((19 - 10) * 2) km² +/// = 2^18 km² +/// = 262,144 km² +/// +/// which is roughly the same area as the entire United Kingdom, and twice the +/// horizontal extent of Dwarf Fortress's largest map. Besides the comparison +/// to other games without infinite or near-infinite maps (like Dwarf Fortress), +/// there are other reasons to choose this as a good maximum size: +/// +/// * It is large enough to include geological features of fairly realistic +/// scale. It may be hard to do justice to truly enormous features like the +/// Amazon River, and natural temperature variation not related to altitude +/// would probably not produce climate extremes on an Earth-like planet, but +/// it can comfortably fit enormous river basins, Everest-scale mountains, +/// large islands and inland lakes, vast forests and deserts, and so on. +/// +/// * It is large enough that making it from one side of the map to another will +/// take a *very* long time. We show this with two examples. In each +/// example, travel is either purely horizontal or purely vertical (to +/// minimize distance traveled) across the whole map, and we assume there are +/// no obstacles or slopes. +/// +/// In example 1, a human is walking at the (real-time) speed of the fastest +/// marathon runners (around 6 blocks / real-time s). We assume the human can +/// maintain this pace indefinitely without stopping. Then crossing the map +/// will take about: +/// +/// 2^19 blocks * 1 real-time s / 6 blocks * 1 real-time min / 60 real-time s +/// * 1 real-time hr / 60 real-time min * 1 real-time days / 24 hr = 2^19 / 6 / +/// 60 / 60 / 24 real-time days ≌ 1 real-time day. +/// +/// That's right--it will take a full day of *real* time to cross the map at +/// an apparent speed of 6 m / s. Moreover, since in-game time passes at a +/// rate of 1 in-game min / 1 in-game s, this would also take *60 days* of +/// in-game time. +/// +/// Still though, this is the rate of an ordinary human. And besides that, if +/// we instead had a marathon runner traveling at 6 m / in-game s, it would +/// take just 1 day of in-game time for the runner to cross the map, or a mere +/// 0.4 hr of real time. To show that this rate of travel is unrealistic (and +/// perhaps make an eventual argument for a slower real-time to in-game time +/// conversion rate), our second example will consist of a high-speed train +/// running at 300 km / real-time h (the fastest real-world high speed train +/// averages under 270 k m / h, with 300 km / h as the designed top speed). +/// For a train traveling at this apparent speed (in real time), crossing the +/// map would take: +/// +/// 2^19 blocks * 1 km / 1000 blocks * 1 real-time hr / 300 km +/// = 2^19 / 1000 / 300 real-time hr +/// ≌ 1.75 real-time hr +/// +/// = 2^19 / 1000 / 300 real-time hr * 60 in-game hr / real-time hr +/// * 1 in-game days / 24 in-game hr +/// = 2^19 / 1000 / 300 * 60 / 24 in-game days +/// ≌ 4.37 in-game days +/// +/// In other words, something faster in real-time than any existing high-speed +/// train would be over 4 times slower (in real-time) than our hypothetical +/// top marathon runner running at 6 m / s in in-game speed. This suggests +/// that the gap between in-game time and real-time is probably much too large +/// for most purposes; however, what it definitely shows is that even +/// extremely fast in-game transport across the world will not trivialize its +/// size. +/// +/// It follows that cities or towns of realistic scale, player housing, +/// fields, and so on, will all fit comfortably on a map of this size, while +/// at the same time still being reachable by non-warping, in-game mechanisms +/// (such as high-speed transit). It also provides plenty of room for mounts +/// of varying speeds, which can help ensure that players don't feel cramped or +/// deliberately slowed down by their own speed. +/// +/// * It is small enough that it is (barely) plausible that we could still +/// generate maps for a world of this size using detailed and realistic +/// erosion algorithms. At 1/4 of this map size along each dimension, +/// generation currently takes around 5 hours on a good computer, and one +/// could imagine (since the bottleneck step appears to be roughly O(n)) that +/// with a smart implementation generation times of under a week could be +/// achievable. +/// +/// * The map extends further than the resolution of human eyesight under +/// Earthlike conditions, even from tall mountains across clear landscapes. +/// According to one calculation, even from Mt. Everest in the absence of +/// cloud cover, you could only see for about 339 km before the Earth's +/// horizon prevented you from seeing further, and other sources suggest that +/// in practice the limit is closer to 160 km under realistic conditions. This +/// implies that making the map much larger in a realistic way would require +/// incorporating curvature, and also implies that any features that cannot +/// fit on the map would not (under realistic atmospheric conditions) be fully +/// visible from any point on Earth. Therefore, even if we cannot represent +/// features larger than this accurately, nothing should be amiss from a +/// visual perspective, so this should not significantly impact the player +/// experience. +pub const MAX_WORLD_BLOCKS_LG: Vec2 = Vec2 { x: 19, y: 19 }; + +/// Base two logarithm of a world size, in chunks, per dimension +/// (each dimension must be a power of 2, so the logarithm is exact). +/// +/// NOTE: As an invariant, each dimension must be between 0 and +/// `[MAX_WORLD_BLOCKS_LG] - [TERRAIN_CHUNK_BLOCKS_LG]`. +/// +/// NOTE: As an invariant, `(1 << ([DEFAULT_WORLD_CHUNKS_LG] + +/// [TERRAIN_CHUNK_BLOCKS_LG]))` fits in an i32 (derived from the invariant +/// on [MAX_WORLD_BLOCKS_LG]). +/// +/// NOTE: As an invariant, each dimension (in chunks) must fit in a u16. +/// +/// NOTE: As an invariant, the product of dimensions (in chunks) must fit in a +/// usize. +/// +/// These invariants are all checked on construction of a `MapSizeLg`. +#[derive(Clone, Copy, Debug)] +pub struct MapSizeLg(Vec2); + +impl MapSizeLg { + /// Construct a new `MapSizeLg`, returning an error if the needed invariants + /// do not hold and the vector otherwise. + /// + /// TODO: In the future, we may use unsafe code to assert to the compiler + /// that these invariants indeed hold, safely opening up optimizations + /// that might not otherwise be available at runtime. + #[inline(always)] + pub const fn new(map_size_lg: Vec2) -> Result { + // Assertion on dimensions: must be between + // 0 and MAX_WORLD_BLOCKS_LG] - [TERRAIN_CHUNK_BLOCKS_LG + let is_le_max = map_size_lg.x <= MAX_WORLD_BLOCKS_LG.x - TERRAIN_CHUNK_BLOCKS_LG + && map_size_lg.y <= MAX_WORLD_BLOCKS_LG.y - TERRAIN_CHUNK_BLOCKS_LG; + // Assertion on dimensions: chunks must fit in a u16. + let chunks_in_range = + /* 1u16.checked_shl(map_size_lg.x).is_some() && + 1u16.checked_shl(map_size_lg.y).is_some(); */ + map_size_lg.x <= 16 && + map_size_lg.y <= 16; + if is_le_max && chunks_in_range { + // Assertion on dimensions: blocks must fit in a i32. + let blocks_in_range = + /* 1i32.checked_shl(map_size_lg.x + TERRAIN_CHUNK_BLOCKS_LG).is_some() && + 1i32.checked_shl(map_size_lg.y + TERRAIN_CHUNK_BLOCKS_LG).is_some(); */ + map_size_lg.x + TERRAIN_CHUNK_BLOCKS_LG < 32 && + map_size_lg.y + TERRAIN_CHUNK_BLOCKS_LG < 32; + // Assertion on dimensions: product of dimensions must fit in a usize. + let chunks_product_in_range = + if let Some(_) = 1usize.checked_shl(map_size_lg.x + map_size_lg.y) { + true + } else { + false + }; + if blocks_in_range && chunks_product_in_range { + // Cleared all invariants. + Ok(MapSizeLg(map_size_lg)) + } else { + Err(()) + } + } else { + Err(()) + } + } + + #[inline(always)] + /// Acquire the `MapSizeLg`'s inner vector. + pub const fn vec(self) -> Vec2 { self.0 } + + #[inline(always)] + /// Get the size of this map in chunks. + pub const fn chunks(self) -> Vec2 { Vec2::new(1 << self.0.x, 1 << self.0.y) } + + /// Get the size of an array of the correct size to hold all chunks. + pub const fn chunks_len(self) -> usize { 1 << (self.0.x + self.0.y) } +} + +impl From for Vec2 { + #[inline(always)] + fn from(size: MapSizeLg) -> Self { size.vec() } +} + +pub struct MapConfig<'a> { + /// Base two logarithm of the chunk dimensions of the base map. + /// Has no default; set explicitly during initial orthographic projection. + pub map_size_lg: MapSizeLg, + /// Dimensions of the window being written to. + /// + /// Defaults to `1 << [MapConfig::map_size_lg]`. + pub dimensions: Vec2, + /// x, y, and z of top left of map. + /// + /// Default x and y are 0.0; no reasonable default for z, so set during + /// initial orthographic projection. + pub focus: Vec3, + /// Altitude is divided by gain and clamped to [0, 1]; thus, decreasing gain + /// makes smaller differences in altitude appear larger. + /// + /// No reasonable default for z; set during initial orthographic projection. + pub gain: f32, + /// `fov` is used for shading purposes and refers to how much impact a + /// change in the z direction has on the perceived slope relative to the + /// same change in x and y. + /// + /// It is stored as cos θ in the range (0, 1\] where θ is the FOV + /// "half-angle" used for perspective projection. At 1.0, we treat it + /// as the limit value for θ = 90 degrees, and use an orthographic + /// projection. + /// + /// Defaults to 1.0. + /// + /// FIXME: This is a hack that tries to incorrectly implement a variant of + /// perspective projection (which generates ∂P/∂x and ∂P/∂y for screen + /// coordinate P by using the hyperbolic function \[assuming frustum of + /// \[l, r, b, t, n, f\], rh coordinates, and output from -1 to 1 in + /// s/t, 0 to 1 in r, and NDC is left-handed \[so visible z ranges from + /// -n to -f\]\]): + /// + /// P.s(x, y, z) = -1 + 2(-n/z x - l) / ( r - l) + /// P.t(x, y, z) = -1 + 2(-n/z y - b) / ( t - b) + /// P.r(x, y, z) = 0 + -f(-n/z - 1) / ( f - n) + /// + /// Then arbitrarily using W_e_x = (r - l) as the width of the projected + /// image, we have W_e_x = 2 n_e tan θ ⇒ tan Θ = (r - l) / (2n_e), for a + /// perspective projection + /// + /// (where θ is the half-angle of the FOV). + /// + /// Taking the limit as θ → 90, we show that this degenrates to an + /// orthogonal projection: + /// + /// lim{n → ∞}(-f(-n / z - 1) / (f - n)) = -(z - -n) / (f - n). + /// + /// (Proof not currently included, but has been formalized for the P.r case + /// in Coq-tactic notation; the proof can be added on request, but is + /// large and probably not well-suited to Rust documentation). + /// + /// For this reason, we feel free to store `fov` as cos θ in the range (0, + /// 1\]. + /// + /// However, `fov` does not actually work properly yet, so for now we just + /// treat it as a visual gimmick. + pub fov: f64, + /// Scale is like gain, but for x and y rather than z. + /// + /// Defaults to (1 << world_size_lg).x / dimensions.x (NOTE: fractional, not + /// integer, division!). + pub scale: f64, + /// Vector that indicates which direction light is coming from, if shading + /// is turned on. + /// + /// Right-handed coordinate system: light is going left, down, and + /// "backwards" (i.e. on the map, where we translate the y coordinate on + /// the world map to z in the coordinate system, the light comes from -y + /// on the map and points towards +y on the map). In a right + /// handed coordinate system, the "camera" points towards -z, so positive z + /// is backwards "into" the camera. + /// + /// "In world space the x-axis will be pointing east, the y-axis up and the + /// z-axis will be pointing south" + /// + /// Defaults to (-0.8, -1.0, 0.3). + pub light_direction: Vec3, + /// If Some, uses the provided horizon map. + /// + /// Defaults to None. + pub horizons: Option<&'a [(Vec, Vec); 2]>, + /// If true, only the basement (bedrock) is used for altitude; otherwise, + /// the surface is used. + /// + /// Defaults to false. + pub is_basement: bool, + /// If true, water is rendered; otherwise, the surface without water is + /// rendered, even if it is underwater. + /// + /// Defaults to true. + pub is_water: bool, + /// If true, 3D lighting and shading are turned on. Otherwise, a plain + /// altitude map is used. + /// + /// Defaults to true. + pub is_shaded: bool, + /// If true, the red component of the image is also used for temperature + /// (redder is hotter). Defaults to false. + pub is_temperature: bool, + /// If true, the blue component of the image is also used for humidity + /// (bluer is wetter). + /// + /// Defaults to false. + pub is_humidity: bool, + /// Record debug information. + /// + /// Defaults to false. + pub is_debug: bool, +} + +pub const QUADRANTS: usize = 4; + +pub struct MapDebug { + pub quads: [[u32; QUADRANTS]; QUADRANTS], + pub rivers: u32, + pub lakes: u32, + pub oceans: u32, +} + +/// Connection kind (per edge). Currently just supports rivers, but may be +/// extended to support paths or at least one other kind of connection. +#[derive(Clone, Copy, Debug)] +pub enum ConnectionKind { + /// Connection forms a visible river. + River, +} + +/// Map connection (per edge). +#[derive(Clone, Copy, Debug)] +pub struct Connection { + /// The kind of connection this is (e.g. river or path). + pub kind: ConnectionKind, + /// Assumed to be the "b" part of a 2d quadratic function. + pub spline_derivative: Vec2, + /// Width of the connection. + pub width: f32, +} + +/// Per-chunk data the map needs to be able to sample in order to correctly +/// render. +#[derive(Clone, Debug)] +pub struct MapSample { + /// the base RGB color for a particular map pixel using the current settings + /// (i.e. the color *without* lighting). + pub rgb: Rgb, + /// Surface altitude information + /// (correctly reflecting settings like is_basement and is_water) + pub alt: f64, + /// Downhill chunk (may not be meaningful on ocean tiles, or at least edge + /// tiles) + pub downhill_wpos: Vec2, + /// Connection information about any connections to/from this chunk (e.g. + /// rivers). + /// + /// Connections at each index correspond to the same index in + /// NEIGHBOR_DELTA. + pub connections: Option<[Option; 8]>, +} + +impl<'a> MapConfig<'a> { + /// Constructs the configuration settings for an orthographic projection of + /// a map from the top down, rendering (by default) the complete map to + /// an image such that the chunk:pixel ratio is 1:1. + /// + /// Takes two arguments: the base two logarithm of the horizontal map extent + /// (in chunks), and the z bounds of the projection. + pub fn orthographic(map_size_lg: MapSizeLg, z_bounds: RangeInclusive) -> Self { + assert!(z_bounds.start() <= z_bounds.end()); + // NOTE: Safe cast since map_size_lg is restricted by the prior assert. + let dimensions = map_size_lg.chunks().map(usize::from); + Self { + map_size_lg, + dimensions, + focus: Vec3::new(0.0, 0.0, f64::from(*z_bounds.start())), + gain: z_bounds.end() - z_bounds.start(), + fov: 1.0, + scale: 1.0, + light_direction: Vec3::new(-1.2, -1.0, 0.8), + horizons: None, + + is_basement: false, + is_water: true, + is_shaded: true, + is_temperature: false, + is_humidity: false, + is_debug: false, + } + } + + /// Get the base 2 logarithm of the underlying map size. + pub fn map_size_lg(&self) -> MapSizeLg { self.map_size_lg } + + /// Generates a map image using the specified settings. Note that it will + /// write from left to write from (0, 0) to dimensions - 1, inclusive, + /// with 4 1-byte color components provided as (r, g, b, a). It is up + /// to the caller to provide a function that translates this information + /// into the correct format for a buffer and writes to it. + /// + /// sample_pos is a function that, given a chunk position, returns enough + /// information about the chunk to attempt to render it on the map. + /// When in doubt, try using `MapConfig::sample_pos` for this. + /// + /// sample_wpos is a simple function that, given a *column* position, + /// returns the approximate altitude at that column. When in doubt, try + /// using `MapConfig::sample_wpos` for this. + #[allow(clippy::if_same_then_else)] // TODO: Pending review in #587 + #[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587 + #[allow(clippy::many_single_char_names)] + pub fn generate( + &self, + sample_pos: impl Fn(Vec2) -> MapSample, + sample_wpos: impl Fn(Vec2) -> f32, + mut write_pixel: impl FnMut(Vec2, (u8, u8, u8, u8)), + ) -> MapDebug { + let MapConfig { + map_size_lg, + dimensions, + focus, + gain, + fov, + scale, + light_direction, + horizons, + + is_shaded, + // is_debug, + .. + } = *self; + + let light_direction = Vec3::new( + light_direction.x, + light_direction.y, + 0.0, // we currently ignore light_direction.z. + ); + let light_shadow_dir = if light_direction.x >= 0.0 { 0 } else { 1 }; + let horizon_map = horizons.map(|horizons| &horizons[light_shadow_dir]); + let light = light_direction.normalized(); + let /*mut */quads = [[0u32; QUADRANTS]; QUADRANTS]; + let /*mut */rivers = 0u32; + let /*mut */lakes = 0u32; + let /*mut */oceans = 0u32; + + let focus_rect = Vec2::from(focus); + + let chunk_size = TerrainChunkSize::RECT_SIZE.map(|e| e as f64); + + /* // NOTE: Asserting this to enable LLVM optimizations. Ideally we should come up + // with a principled way to do this (especially one with no runtime + // cost). + assert!( + map_size_lg + .vec() + .cmple(&(MAX_WORLD_BLOCKS_LG - TERRAIN_CHUNK_BLOCKS_LG)) + .reduce_and() + ); */ + let world_size = map_size_lg.chunks(); + + (0..dimensions.y * dimensions.x).for_each(|chunk_idx| { + let i = chunk_idx % dimensions.x as usize; + let j = chunk_idx / dimensions.x as usize; + + let wposf = focus_rect + Vec2::new(i as f64, j as f64) * scale; + let pos = wposf.map(|e: f64| e as i32); + let wposf = wposf * chunk_size; + + let chunk_idx = if pos.reduce_partial_min() >= 0 + && pos.x < world_size.x as i32 + && pos.y < world_size.y as i32 + { + Some(vec2_as_uniform_idx(map_size_lg, pos)) + } else { + None + }; + + let MapSample { + rgb, + alt, + downhill_wpos, + .. + } = sample_pos(pos); + + let alt = alt as f32; + let wposi = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let mut rgb = rgb.map(|e| e as f64 / 255.0); + + // Material properties: + // + // For each material in the scene, + // k_s = (RGB) specular reflection constant + let mut k_s = Rgb::new(1.0, 1.0, 1.0); + // k_d = (RGB) diffuse reflection constant + let mut k_d = rgb; + // k_a = (RGB) ambient reflection constant + let mut k_a = rgb; + // α = (per-material) shininess constant + let mut alpha = 4.0; // 4.0; + + // Compute connections + let mut has_river = false; + // NOTE: consider replacing neighbors with local_cells, since it is more + // accurate (though I'm not sure if it can matter for these + // purposes). + chunk_idx + .map(|chunk_idx| neighbors(map_size_lg, chunk_idx).chain(iter::once(chunk_idx))) + .into_iter() + .flatten() + .for_each(|neighbor_posi| { + let neighbor_pos = uniform_idx_as_vec2(map_size_lg, neighbor_posi); + let neighbor_wpos = neighbor_pos.map(|e| e as f64) * chunk_size; + let MapSample { connections, .. } = sample_pos(neighbor_pos); + NEIGHBOR_DELTA + .iter() + .zip( + connections + .as_ref() + .map(|e| e.iter()) + .into_iter() + .flatten() + .into_iter(), + ) + .for_each(|(&delta, connection)| { + let connection = if let Some(connection) = connection { + connection + } else { + return; + }; + let downhill_wpos = neighbor_wpos + + Vec2::from(delta).map(|e: i32| e as f64) * chunk_size; + let coeffs = river_spline_coeffs( + neighbor_wpos, + connection.spline_derivative, + downhill_wpos, + ); + let (_t, _pt, dist) = if let Some((t, pt, dist)) = + quadratic_nearest_point(&coeffs, wposf) + { + (t, pt, dist) + } else { + let ndist = wposf.distance_squared(neighbor_wpos); + let ddist = wposf.distance_squared(downhill_wpos); + if ndist <= ddist { + (0.0, neighbor_wpos, ndist) + } else { + (1.0, downhill_wpos, ddist) + } + }; + let connection_dist = + (dist.sqrt() - (connection.width as f64 * 0.5).max(1.0)).max(0.0); + if connection_dist == 0.0 { + match connection.kind { + ConnectionKind::River => { + has_river = true; + }, + } + } + }); + }); + + // Color in connectins. + let water_color_factor = 2.0; + let g_water = 32.0 * water_color_factor; + let b_water = 64.0 * water_color_factor; + if has_river { + let water_rgb = Rgb::new(0, ((g_water) * 1.0) as u8, ((b_water) * 1.0) as u8) + .map(|e| e as f64 / 255.0); + rgb = water_rgb; + k_s = Rgb::new(1.0, 1.0, 1.0); + k_d = water_rgb; + k_a = water_rgb; + alpha = 0.255; + } + + let downhill_alt = sample_wpos(downhill_wpos); + let cross_pos = wposi + + ((downhill_wpos - wposi) + .map(|e| e as f32) + .rotated_z(f32::consts::FRAC_PI_2) + .map(|e| e as i32)); + let cross_alt = sample_wpos(cross_pos); + // TODO: Fix use of fov to match proper perspective projection, as described in + // the doc comment. + // Pointing downhill, forward + // (index--note that (0,0,1) is backward right-handed) + let forward_vec = Vec3::new( + (downhill_wpos.x - wposi.x) as f64, + ((downhill_alt - alt) * gain) as f64 * fov, + (downhill_wpos.y - wposi.y) as f64, + ); + // Pointing 90 degrees left (in horizontal xy) of downhill, up + // (middle--note that (1,0,0), 90 degrees CCW backward, is right right-handed) + let up_vec = Vec3::new( + (cross_pos.x - wposi.x) as f64, + ((cross_alt - alt) * gain) as f64 * fov, + (cross_pos.y - wposi.y) as f64, + ); + // let surface_normal = Vec3::new(fov* (f.y * u.z - f.z * u.y), -(f.x * u.z - + // f.z * u.x), fov* (f.x * u.y - f.y * u.x)).normalized(); + // Then cross points "to the right" (upwards) on a right-handed coordinate + // system. (right-handed coordinate system means (0, 0, 1.0) is + // "forward" into the screen). + let surface_normal = forward_vec.cross(up_vec).normalized(); + + // TODO: Figure out if we can reimplement debugging. + /* if is_debug { + let quad = + |x: f32| ((x as f64 * QUADRANTS as f64).floor() as usize).min(QUADRANTS - 1); + if river_kind.is_none() || humidity != 0.0 { + quads[quad(humidity)][quad(temperature)] += 1; + } + match river_kind { + Some(RiverKind::River { .. }) => { + rivers += 1; + }, + Some(RiverKind::Lake { .. }) => { + lakes += 1; + }, + Some(RiverKind::Ocean { .. }) => { + oceans += 1; + }, + None => {}, + } + } */ + + let shade_frac = horizon_map + .and_then(|(angles, heights)| { + chunk_idx + .and_then(|chunk_idx| angles.get(chunk_idx)) + .map(|&e| (e as f64, heights)) + }) + .and_then(|(e, heights)| { + chunk_idx + .and_then(|chunk_idx| heights.get(chunk_idx)) + .map(|&f| (e, f as f64)) + }) + .map(|(angle, height)| { + let w = 0.1; + let height = (height - f64::from(alt * gain)).max(0.0); + if angle != 0.0 && light_direction.x != 0.0 && height != 0.0 { + let deltax = height / angle; + let lighty = (light_direction.y / light_direction.x * deltax).abs(); + let deltay = lighty - height; + let s = (deltay / deltax / w).min(1.0).max(0.0); + // Smoothstep + s * s * (3.0 - 2.0 * s) + } else { + 1.0 + } + }) + .unwrap_or(1.0); + + let rgb = if is_shaded { + // Phong reflection model with shadows: + // + // I_p = k_a i_a + shadow * Σ {m ∈ lights} (k_d (L_m ⋅ N) i_m,d + k_s (R_m ⋅ + // V)^α i_m,s) + // + // where for the whole scene, + // i_a = (RGB) intensity of ambient lighting component + let i_a = Rgb::new(0.1, 0.1, 0.1); + // V = direction pointing towards the viewer (e.g. virtual camera). + let v = Vec3::new(0.0, 0.0, -1.0).normalized(); + // let v = Vec3::new(0.0, -1.0, 0.0).normalized(); + // + // for each light m, + // i_m,d = (RGB) intensity of diffuse component of light source m + let i_m_d = Rgb::new(1.0, 1.0, 1.0); + // i_m,s = (RGB) intensity of specular component of light source m + let i_m_s = Rgb::new(0.45, 0.45, 0.45); + // let i_m_s = Rgb::new(0.45, 0.45, 0.45); + + // for each light m and point p, + // L_m = (normalized) direction vector from point on surface to light source m + let l_m = light; + // N = (normalized) normal at this point on the surface, + let n = surface_normal; + // R_m = (normalized) direction a perfectly reflected ray of light from m would + // take from point p = 2(L_m ⋅ N)N - L_m + let r_m = (-l_m).reflected(n); // 2 * (l_m.dot(n)) * n - l_m; + // + // and for each point p in the scene, + // shadow = computed shadow factor at point p + // FIXME: Should really just be shade_frac, but with only ambient light we lose + // all local lighting detail... some sort of global illumination (e.g. + // radiosity) is of course the "right" solution, but maybe we can find + // something cheaper? + let shadow = 0.2 + 0.8 * shade_frac; + + let lambertian = l_m.dot(n).max(0.0); + let spec_angle = r_m.dot(v).max(0.0); + + let ambient = k_a * i_a; + let diffuse = k_d * lambertian * i_m_d; + let specular = k_s * spec_angle.powf(alpha) * i_m_s; + (ambient + shadow * (diffuse + specular)).map(|e| e.min(1.0)) + } else { + rgb + } + .map(|e| (e * 255.0) as u8); + + let rgba = (rgb.r, rgb.g, rgb.b, 255); + write_pixel(Vec2::new(i, j), rgba); + }); + + MapDebug { + quads, + rivers, + lakes, + oceans, + } + } +} diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index 55e7b56013..ada6b7ab85 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -1,14 +1,17 @@ pub mod biome; pub mod block; pub mod chonk; +pub mod map; pub mod structure; // Reexports pub use self::{ biome::BiomeKind, block::{Block, BlockKind}, + map::MapSizeLg, structure::Structure, }; +use roots::find_roots_cubic; use serde::{Deserialize, Serialize}; use crate::{vol::RectVolSize, volumes::vol_grid_2d::VolGrid2d}; @@ -19,8 +22,24 @@ use vek::*; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TerrainChunkSize; +/// Base two logarithm of the number of blocks along either horizontal axis of +/// a chunk. +/// +/// NOTE: (1 << CHUNK_SIZE_LG) is guaranteed to fit in a u32. +/// +/// NOTE: A lot of code assumes that the two dimensions are equal, so we make it +/// explicit here. +/// +/// NOTE: It is highly unlikely that a value greater than 5 will work, as many +/// frontend optimizations rely on being able to pack chunk horizontal +/// dimensions into 5 bits each. +pub const TERRAIN_CHUNK_BLOCKS_LG: u32 = 5; + impl RectVolSize for TerrainChunkSize { - const RECT_SIZE: Vec2 = Vec2 { x: 32, y: 32 }; + const RECT_SIZE: Vec2 = Vec2 { + x: (1 << TERRAIN_CHUNK_BLOCKS_LG), + y: (1 << TERRAIN_CHUNK_BLOCKS_LG), + }; } // TerrainChunkMeta @@ -50,3 +69,140 @@ impl TerrainChunkMeta { pub type TerrainChunk = chonk::Chonk; pub type TerrainGrid = VolGrid2d; + +// Terrain helper functions used across multiple crates. + +/// Computes the position Vec2 of a SimChunk from an index, where the index was +/// generated by uniform_noise. +/// +/// NOTE: Dimensions obey constraints on [map::MapConfig::map_size_lg]. +#[inline(always)] +pub fn uniform_idx_as_vec2(map_size_lg: MapSizeLg, idx: usize) -> Vec2 { + let x_mask = (1 << map_size_lg.vec().x) - 1; + Vec2::new((idx & x_mask) as i32, (idx >> map_size_lg.vec().x) as i32) +} + +/// Computes the index of a Vec2 of a SimChunk from a position, where the index +/// is generated by uniform_noise. NOTE: Both components of idx should be +/// in-bounds! +#[inline(always)] +pub fn vec2_as_uniform_idx(map_size_lg: MapSizeLg, idx: Vec2) -> usize { + ((idx.y as usize) << map_size_lg.vec().x) | idx.x as usize +} + +// NOTE: want to keep this such that the chunk index is in ascending order! +pub const NEIGHBOR_DELTA: [(i32, i32); 8] = [ + (-1, -1), + (0, -1), + (1, -1), + (-1, 0), + (1, 0), + (-1, 1), + (0, 1), + (1, 1), +]; + +/// Iterate through all cells adjacent to a chunk. +#[inline(always)] +pub fn neighbors(map_size_lg: MapSizeLg, posi: usize) -> impl Clone + Iterator { + let pos = uniform_idx_as_vec2(map_size_lg, posi); + let world_size = map_size_lg.chunks(); + NEIGHBOR_DELTA + .iter() + .map(move |&(x, y)| Vec2::new(pos.x + x, pos.y + y)) + .filter(move |pos| { + pos.x >= 0 && pos.y >= 0 && pos.x < world_size.x as i32 && pos.y < world_size.y as i32 + }) + .map(move |pos| vec2_as_uniform_idx(map_size_lg, pos)) +} + +pub fn river_spline_coeffs( + // _sim: &WorldSim, + chunk_pos: Vec2, + spline_derivative: Vec2, + downhill_pos: Vec2, +) -> Vec3> { + let dxy = downhill_pos - chunk_pos; + // Since all splines have been precomputed, we don't have to do that much work + // to evaluate the spline. The spline is just ax^2 + bx + c = 0, where + // + // a = dxy - chunk.river.spline_derivative + // b = chunk.river.spline_derivative + // c = chunk_pos + let spline_derivative = spline_derivative.map(|e| e as f64); + Vec3::new(dxy - spline_derivative, spline_derivative, chunk_pos) +} + +/// Find the nearest point from a quadratic spline to this point (in terms of t, +/// the "distance along the curve" by which our spline is parameterized). Note +/// that if t < 0.0 or t >= 1.0, we probably shouldn't be considered "on the +/// curve"... hopefully this works out okay and gives us what we want (a +/// river that extends outwards tangent to a quadratic curve, with width +/// configured by distance along the line). +#[allow(clippy::let_and_return)] // TODO: Pending review in #587 +#[allow(clippy::many_single_char_names)] +pub fn quadratic_nearest_point( + spline: &Vec3>, + point: Vec2, +) -> Option<(f64, Vec2, f64)> { + let a = spline.z.x; + let b = spline.y.x; + let c = spline.x.x; + let d = point.x; + let e = spline.z.y; + let f = spline.y.y; + let g = spline.x.y; + let h = point.y; + // This is equivalent to solving the following cubic equation (derivation is a + // bit annoying): + // + // A = 2(c^2 + g^2) + // B = 3(b * c + g * f) + // C = ((a - d) * 2 * c + b^2 + (e - h) * 2 * g + f^2) + // D = ((a - d) * b + (e - h) * f) + // + // Ax³ + Bx² + Cx + D = 0 + // + // Once solved, this yield up to three possible values for t (reflecting minimal + // and maximal values). We should choose the minimal such real value with t + // between 0.0 and 1.0. If we fall outside those bounds, then we are + // outside the spline and return None. + let a_ = (c * c + g * g) * 2.0; + let b_ = (b * c + g * f) * 3.0; + let a_d = a - d; + let e_h = e - h; + let c_ = a_d * c * 2.0 + b * b + e_h * g * 2.0 + f * f; + let d_ = a_d * b + e_h * f; + let roots = find_roots_cubic(a_, b_, c_, d_); + let roots = roots.as_ref(); + + let min_root = roots + .iter() + .copied() + .filter_map(|root| { + let river_point = spline.x * root * root + spline.y * root + spline.z; + let river_zero = spline.z; + let river_one = spline.x + spline.y + spline.z; + if root > 0.0 && root < 1.0 { + Some((root, river_point)) + } else if river_point.distance_squared(river_zero) < 0.5 { + Some((root, /*river_point*/ river_zero)) + } else if river_point.distance_squared(river_one) < 0.5 { + Some((root, /*river_point*/ river_one)) + } else { + None + } + }) + .map(|(root, river_point)| { + let river_distance = river_point.distance_squared(point); + (root, river_point, river_distance) + }) + // In the (unlikely?) case that distances are equal, prefer the earliest point along the + // river. + .min_by(|&(ap, _, a), &(bp, _, b)| { + (a, ap < 0.0 || ap > 1.0, ap) + .partial_cmp(&(b, bp < 0.0 || bp > 1.0, bp)) + .unwrap() + }); + min_root +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 798bc7f0a3..de17f2663f 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -54,14 +54,14 @@ use std::{ time::{Duration, Instant}, }; #[cfg(not(feature = "worldgen"))] -use test_world::{World, WORLD_SIZE}; +use test_world::World; use tracing::{debug, error, info, warn}; use uvth::{ThreadPool, ThreadPoolBuilder}; use vek::*; #[cfg(feature = "worldgen")] use world::{ civ::SiteKind, - sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP, WORLD_SIZE}, + sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP}, World, }; @@ -200,7 +200,7 @@ impl Server { // complaining) // spawn in the chunk, that is in the middle of the world - let center_chunk: Vec2 = WORLD_SIZE.map(|e| e as i32) / 2; + let center_chunk: Vec2 = world.sim().map_size_lg().chunks().map(i32::from) / 2; // Find a town to spawn in that's close to the centre of the world let spawn_chunk = world diff --git a/server/src/test_world.rs b/server/src/test_world.rs index 1b6d070e61..77f01d91c1 100644 --- a/server/src/test_world.rs +++ b/server/src/test_world.rs @@ -1,13 +1,18 @@ use common::{ generation::{ChunkSupplement, EntityInfo, EntityKind}, - terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, + terrain::{Block, BlockKind, MapSizeLg, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, vol::{ReadVol, RectVolSize, Vox, WriteVol}, }; use rand::{prelude::*, rngs::SmallRng}; use std::time::Duration; use vek::*; -pub const WORLD_SIZE: Vec2 = Vec2 { x: 1, y: 1 }; +const DEFAULT_WORLD_CHUNKS_LG: MapSizeLg = + if let Ok(map_size_lg) = MapSizeLg::new(Vec2 { x: 1, y: 1 }) { + map_size_lg + } else { + panic!("Default world chunk size does not satisfy required invariants."); + }; pub struct World; @@ -16,6 +21,9 @@ impl World { pub fn tick(&self, dt: Duration) {} + #[inline(always)] + pub const fn map_size_lg(&self) -> MapSizeLg { DEFAULT_WORLD_CHUNKS_LG } + pub fn generate_chunk( &self, chunk_pos: Vec2, diff --git a/world/Cargo.toml b/world/Cargo.toml index 3d4ad194a6..f103a98602 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -23,7 +23,6 @@ rand_chacha = "0.2.1" arr_macro = "0.1.2" packed_simd = "0.3.3" rayon = "^1.3.0" -roots = "0.0.5" serde = { version = "1.0.110", features = ["derive"] } ron = { version = "0.6", default-features = false } diff --git a/world/examples/water.rs b/world/examples/water.rs index 50a5a6cd8c..a462b3d709 100644 --- a/world/examples/water.rs +++ b/world/examples/water.rs @@ -1,16 +1,19 @@ -use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use common::{ + terrain::{ + map::{MapConfig, MapDebug, MapSample}, + uniform_idx_as_vec2, vec2_as_uniform_idx, TerrainChunkSize, + }, + vol::RectVolSize, +}; use rayon::prelude::*; use std::{f64, io::Write, path::PathBuf, time::SystemTime}; use tracing::warn; use tracing_subscriber; use vek::*; use veloren_world::{ - sim::{ - self, get_horizon_map, uniform_idx_as_vec2, vec2_as_uniform_idx, MapConfig, MapDebug, - MapSample, WorldOpts, WORLD_SIZE, - }, + sim::{self, get_horizon_map, sample_pos, sample_wpos, WorldOpts}, util::Sampler, - World, CONFIG, + ColumnSample, World, CONFIG, }; const W: usize = 1024; @@ -41,31 +44,41 @@ fn main() { }); tracing::info!("Sampling data..."); let sampler = world.sim(); + let map_size_lg = sampler.map_size_lg(); let samples_data = { let column_sample = world.sample_columns(); - (0..WORLD_SIZE.product()) + (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| { - column_sample - .get(uniform_idx_as_vec2(posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) + column_sample.get( + uniform_idx_as_vec2(map_size_lg, posi) + * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + ) }) .collect::>() .into_boxed_slice() }; - let refresh_map_samples = |config: &MapConfig| { - (0..WORLD_SIZE.product()) + let refresh_map_samples = |config: &MapConfig, samples: Option<&[Option]>| { + (0..map_size_lg.chunks_len()) .into_par_iter() - .map(|posi| config.sample_pos(sampler, uniform_idx_as_vec2(posi))) + .map(|posi| { + sample_pos( + config, + sampler, + samples, + uniform_idx_as_vec2(map_size_lg, posi), + ) + }) .collect::>() .into_boxed_slice() }; let get_map_sample = |map_samples: &[MapSample], pos: Vec2| { if pos.reduce_partial_min() >= 0 - && pos.x < WORLD_SIZE.x as i32 - && pos.y < WORLD_SIZE.y as i32 + && pos.x < map_size_lg.chunks().x as i32 + && pos.y < map_size_lg.chunks().y as i32 { - map_samples[vec2_as_uniform_idx(pos)].clone() + map_samples[vec2_as_uniform_idx(map_size_lg, pos)].clone() } else { MapSample { alt: 0.0, @@ -76,26 +89,26 @@ fn main() { } }; - let refresh_horizons = |lgain, is_basement, is_water| { + let refresh_horizons = |is_basement, is_water| { get_horizon_map( - lgain, + map_size_lg, Aabr { min: Vec2::zero(), - max: WORLD_SIZE.map(|e| e as i32), + max: map_size_lg.chunks().map(|e| e as i32), }, - CONFIG.sea_level as f64, - (CONFIG.sea_level + sampler.max_height) as f64, + CONFIG.sea_level, + CONFIG.sea_level + sampler.max_height, |posi| { - let sample = sampler.get(uniform_idx_as_vec2(posi)).unwrap(); + let sample = sampler.get(uniform_idx_as_vec2(map_size_lg, posi)).unwrap(); if is_basement { - sample.alt as f64 + sample.alt } else { - sample.basement as f64 + sample.basement } .max(if is_water { - sample.water_alt as f64 + sample.water_alt } else { - -f64::INFINITY + -f32::INFINITY }) }, |a| a, @@ -114,8 +127,8 @@ fn main() { // makes smaller differences in altitude appear larger. let mut gain = /*CONFIG.mountain_scale*/sampler.max_height; // The Z component during normal calculations is multiplied by gain; thus, - let mut lgain = 1.0; - let mut scale = WORLD_SIZE.x as f64 / W as f64; + let mut fov = 1.0; + let mut scale = map_size_lg.chunks().x as f64 / W as f64; // Right-handed coordinate system: light is going left, down, and "backwards" // (i.e. on the map, where we translate the y coordinate on the world map to @@ -133,21 +146,21 @@ fn main() { let mut is_temperature = true; let mut is_humidity = true; - let mut horizons = refresh_horizons(lgain, is_basement, is_water); + let mut horizons = refresh_horizons(is_basement, is_water); let mut samples = None; let mut samples_changed = true; let mut map_samples: Box<[_]> = Box::new([]); while win.is_open() { let config = MapConfig { + map_size_lg, dimensions: Vec2::new(W, H), focus, gain, - lgain, + fov, scale, light_direction, horizons: horizons.as_ref(), /* .map(|(a, b)| (&**a, &**b)) */ - samples, is_basement, is_water, @@ -158,7 +171,7 @@ fn main() { }; if samples_changed { - map_samples = refresh_map_samples(&config); + map_samples = refresh_map_samples(&config, samples); }; let mut buf = vec![0; W * H]; @@ -169,7 +182,7 @@ fn main() { quads, } = config.generate( |pos| get_map_sample(&map_samples, pos), - |pos| config.sample_wpos(sampler, pos), + |pos| sample_wpos(&config, sampler, pos), |pos, (r, g, b, a)| { let i = pos.x; let j = pos.y; @@ -187,7 +200,7 @@ fn main() { { let x = (W as f64 * scale) as usize; let y = (H as f64 * scale) as usize; - let config = sim::MapConfig { + let config = MapConfig { dimensions: Vec2::new(x, y), scale: 1.0, ..config @@ -195,7 +208,7 @@ fn main() { let mut buf = vec![0u8; 4 * len]; config.generate( |pos| get_map_sample(&map_samples, pos), - |pos| config.sample_wpos(sampler, pos), + |pos| sample_wpos(&config, sampler, pos), |pos, (r, g, b, a)| { let i = pos.x; let j = pos.y; @@ -233,7 +246,7 @@ fn main() { Land(adjacent): (X = temp, Y = humidity): {:?}\nRivers: {:?}\nLakes: \ {:?}\nOceans: {:?}\nTotal water: {:?}\nTotal land(adjacent): {:?}", gain, - lgain, + fov, scale, focus, light_direction, @@ -272,7 +285,7 @@ fn main() { if win.is_key_down(minifb::Key::B) { is_basement ^= true; samples_changed = true; - horizons = horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); + horizons = horizons.and_then(|_| refresh_horizons(is_basement, is_water)); } if win.is_key_down(minifb::Key::H) { is_humidity ^= true; @@ -285,7 +298,7 @@ fn main() { if win.is_key_down(minifb::Key::O) { is_water ^= true; samples_changed = true; - horizons = horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); + horizons = horizons.and_then(|_| refresh_horizons(is_basement, is_water)); } if win.is_key_down(minifb::Key::L) { if is_camera { @@ -293,7 +306,7 @@ fn main() { horizons = if horizons.is_some() { None } else { - refresh_horizons(lgain, is_basement, is_water) + refresh_horizons(is_basement, is_water) }; samples_changed = true; } else { @@ -335,10 +348,8 @@ fn main() { } if win.is_key_down(minifb::Key::Q) { if is_camera { - if (lgain * 2.0).is_normal() { - lgain *= 2.0; - horizons = - horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); + if (fov * 2.0).is_normal() { + fov *= 2.0; } } else { gain += 64.0; @@ -346,10 +357,8 @@ fn main() { } if win.is_key_down(minifb::Key::E) { if is_camera { - if (lgain / 2.0).is_normal() { - lgain /= 2.0; - horizons = - horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); + if (fov / 2.0).is_normal() { + fov /= 2.0; } } else { gain = (gain - 64.0).max(64.0); diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index b6a3e81d36..e389ebad86 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -14,7 +14,7 @@ use common::{ path::Path, spiral::Spiral2d, store::{Id, Store}, - terrain::TerrainChunkSize, + terrain::{MapSizeLg, TerrainChunkSize}, vol::RectVolSize, }; use core::{ @@ -29,7 +29,11 @@ use rand_chacha::ChaChaRng; use tracing::{debug, info, warn}; use vek::*; -const INITIAL_CIV_COUNT: usize = (crate::sim::WORLD_SIZE.x * crate::sim::WORLD_SIZE.y * 3) / 65536; //48 at default scale +const fn initial_civ_count(map_size_lg: MapSizeLg) -> u32 { + // NOTE: since map_size_lg's dimensions must fit in a u16, we can safely add + // them here. NOTE: N48 at "default" scale of 10 × 10 bits. + (3 << (map_size_lg.vec().x + map_size_lg.vec().y)) >> 16 +} #[allow(clippy::type_complexity)] // TODO: Pending review in #587 #[derive(Default)] @@ -74,17 +78,17 @@ impl Civs { pub fn generate(seed: u32, sim: &mut WorldSim) -> Self { let mut this = Self::default(); let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed)); + let initial_civ_count = initial_civ_count(sim.map_size_lg()); let mut ctx = GenCtx { sim, rng }; - - for _ in 0..INITIAL_CIV_COUNT { + for _ in 0..initial_civ_count { debug!("Creating civilisation..."); if this.birth_civ(&mut ctx.reseed()).is_none() { warn!("Failed to find starting site for civilisation."); } } - info!(?INITIAL_CIV_COUNT, "all civilisations created"); + info!(?initial_civ_count, "all civilisations created"); - for _ in 0..INITIAL_CIV_COUNT * 3 { + for _ in 0..initial_civ_count * 3 { attempt(5, || { let loc = find_site_loc(&mut ctx, None)?; this.establish_site(&mut ctx.reseed(), loc, |place| Site { diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 7c9ba23f73..7dda86997c 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -1,13 +1,18 @@ use crate::{ all::ForestKind, block::StructureMeta, - sim::{local_cells, uniform_idx_as_vec2, vec2_as_uniform_idx, RiverKind, SimChunk, WorldSim}, + sim::{local_cells, RiverKind, SimChunk, WorldSim}, util::Sampler, CONFIG, }; -use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use common::{ + terrain::{ + quadratic_nearest_point, river_spline_coeffs, uniform_idx_as_vec2, vec2_as_uniform_idx, + TerrainChunkSize, + }, + vol::RectVolSize, +}; use noise::NoiseFn; -use roots::find_roots_cubic; use std::{ cmp::Reverse, f32, f64, @@ -74,97 +79,6 @@ impl<'a> ColumnGen<'a> { } } -pub fn river_spline_coeffs( - // _sim: &WorldSim, - chunk_pos: Vec2, - spline_derivative: Vec2, - downhill_pos: Vec2, -) -> Vec3> { - let dxy = downhill_pos - chunk_pos; - // Since all splines have been precomputed, we don't have to do that much work - // to evaluate the spline. The spline is just ax^2 + bx + c = 0, where - // - // a = dxy - chunk.river.spline_derivative - // b = chunk.river.spline_derivative - // c = chunk_pos - let spline_derivative = spline_derivative.map(|e| e as f64); - Vec3::new(dxy - spline_derivative, spline_derivative, chunk_pos) -} - -/// Find the nearest point from a quadratic spline to this point (in terms of t, -/// the "distance along the curve" by which our spline is parameterized). Note -/// that if t < 0.0 or t >= 1.0, we probably shouldn't be considered "on the -/// curve"... hopefully this works out okay and gives us what we want (a -/// river that extends outwards tangent to a quadratic curve, with width -/// configured by distance along the line). -#[allow(clippy::let_and_return)] // TODO: Pending review in #587 -#[allow(clippy::many_single_char_names)] -pub fn quadratic_nearest_point( - spline: &Vec3>, - point: Vec2, -) -> Option<(f64, Vec2, f64)> { - let a = spline.z.x; - let b = spline.y.x; - let c = spline.x.x; - let d = point.x; - let e = spline.z.y; - let f = spline.y.y; - let g = spline.x.y; - let h = point.y; - // This is equivalent to solving the following cubic equation (derivation is a - // bit annoying): - // - // A = 2(c^2 + g^2) - // B = 3(b * c + g * f) - // C = ((a - d) * 2 * c + b^2 + (e - h) * 2 * g + f^2) - // D = ((a - d) * b + (e - h) * f) - // - // Ax³ + Bx² + Cx + D = 0 - // - // Once solved, this yield up to three possible values for t (reflecting minimal - // and maximal values). We should choose the minimal such real value with t - // between 0.0 and 1.0. If we fall outside those bounds, then we are - // outside the spline and return None. - let a_ = (c * c + g * g) * 2.0; - let b_ = (b * c + g * f) * 3.0; - let a_d = a - d; - let e_h = e - h; - let c_ = a_d * c * 2.0 + b * b + e_h * g * 2.0 + f * f; - let d_ = a_d * b + e_h * f; - let roots = find_roots_cubic(a_, b_, c_, d_); - let roots = roots.as_ref(); - - let min_root = roots - .iter() - .copied() - .filter_map(|root| { - let river_point = spline.x * root * root + spline.y * root + spline.z; - let river_zero = spline.z; - let river_one = spline.x + spline.y + spline.z; - if root > 0.0 && root < 1.0 { - Some((root, river_point)) - } else if river_point.distance_squared(river_zero) < 0.5 { - Some((root, /*river_point*/ river_zero)) - } else if river_point.distance_squared(river_one) < 0.5 { - Some((root, /*river_point*/ river_one)) - } else { - None - } - }) - .map(|(root, river_point)| { - let river_distance = river_point.distance_squared(point); - (root, river_point, river_distance) - }) - // In the (unlikely?) case that distances are equal, prefer the earliest point along the - // river. - .min_by(|&(ap, _, a), &(bp, _, b)| { - (a, ap < 0.0 || ap > 1.0, ap) - .partial_cmp(&(b, bp < 0.0 || bp > 1.0, bp)) - .unwrap() - }); - min_root -} - impl<'a> Sampler<'a> for ColumnGen<'a> { type Index = Vec2; type Sample = Option>; @@ -196,12 +110,13 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { let chunk_warp_factor = sim.get_interpolated_monotone(wpos, |chunk| chunk.warp_factor)?; let sim_chunk = sim.get(chunk_pos)?; let neighbor_coef = TerrainChunkSize::RECT_SIZE.map(|e| e as f64); - let my_chunk_idx = vec2_as_uniform_idx(chunk_pos); - let neighbor_river_data = local_cells(my_chunk_idx).filter_map(|neighbor_idx: usize| { - let neighbor_pos = uniform_idx_as_vec2(neighbor_idx); - let neighbor_chunk = sim.get(neighbor_pos)?; - Some((neighbor_pos, neighbor_chunk, &neighbor_chunk.river)) - }); + let my_chunk_idx = vec2_as_uniform_idx(self.sim.map_size_lg(), chunk_pos); + let neighbor_river_data = + local_cells(self.sim.map_size_lg(), my_chunk_idx).filter_map(|neighbor_idx: usize| { + let neighbor_pos = uniform_idx_as_vec2(self.sim.map_size_lg(), neighbor_idx); + let neighbor_chunk = sim.get(neighbor_pos)?; + Some((neighbor_pos, neighbor_chunk, &neighbor_chunk.river)) + }); let lake_width = (TerrainChunkSize::RECT_SIZE.x as f64 * (2.0f64.sqrt())) + 12.0; let neighbor_river_data = neighbor_river_data.map(|(posj, chunkj, river)| { let kind = match river.river_kind { diff --git a/world/src/lib.rs b/world/src/lib.rs index 50dfc78b6a..5eaefeedb9 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -1,7 +1,13 @@ #![deny(unsafe_code)] #![allow(incomplete_features)] #![allow(clippy::option_map_unit_fn)] -#![feature(arbitrary_enum_discriminant, const_generics, label_break_value)] +#![feature( + arbitrary_enum_discriminant, + const_if_match, + const_generics, + const_panic, + label_break_value +)] mod all; mod block; @@ -16,9 +22,10 @@ pub mod util; // Reexports pub use crate::config::CONFIG; pub use block::BlockGen; +pub use column::ColumnSample; use crate::{ - column::{ColumnGen, ColumnSample}, + column::ColumnGen, util::{Grid, Sampler}, }; use common::{ diff --git a/world/src/sim/erosion.rs b/world/src/sim/erosion.rs index 046207c8c3..c0c85b0844 100644 --- a/world/src/sim/erosion.rs +++ b/world/src/sim/erosion.rs @@ -1,9 +1,12 @@ -use super::{ - diffusion, downhill, neighbors, uniform_idx_as_vec2, uphill, vec2_as_uniform_idx, - NEIGHBOR_DELTA, WORLD_SIZE, -}; +use super::{diffusion, downhill, uphill}; use crate::{config::CONFIG, util::RandomField}; -use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use common::{ + terrain::{ + neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, MapSizeLg, TerrainChunkSize, + NEIGHBOR_DELTA, + }, + vol::RectVolSize, +}; use tracing::{debug, error, warn}; // use faster::*; use itertools::izip; @@ -27,7 +30,12 @@ pub type Computex8 = [Compute; 8]; /// Compute the water flux at all chunks, given a list of chunk indices sorted /// by increasing height. -pub fn get_drainage(newh: &[u32], downhill: &[isize], _boundary_len: usize) -> Box<[f32]> { +pub fn get_drainage( + map_size_lg: MapSizeLg, + newh: &[u32], + downhill: &[isize], + _boundary_len: usize, +) -> Box<[f32]> { // FIXME: Make the below work. For now, we just use constant flux. // Initially, flux is determined by rainfall. We currently treat this as the // same as humidity, so we just use humidity as a proxy. The total flux @@ -35,10 +43,10 @@ pub fn get_drainage(newh: &[u32], downhill: &[isize], _boundary_len: usize) -> B // to be 0.5. To figure out how far from normal any given chunk is, we use // its logit. NOTE: If there are no non-boundary chunks, we just set // base_flux to 1.0; this should still work fine because in that case - // there's no erosion anyway. let base_flux = 1.0 / ((WORLD_SIZE.x * - // WORLD_SIZE.y) as f32); + // there's no erosion anyway. let base_flux = 1.0 / ((map_size_lg.chunks_len()) + // as f32); let base_flux = 1.0; - let mut flux = vec![base_flux; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut flux = vec![base_flux; map_size_lg.chunks_len()].into_boxed_slice(); newh.iter().rev().for_each(|&chunk_idx| { let chunk_idx = chunk_idx as usize; let downhill_idx = downhill[chunk_idx]; @@ -52,6 +60,7 @@ pub fn get_drainage(newh: &[u32], downhill: &[isize], _boundary_len: usize) -> B /// Compute the water flux at all chunks for multiple receivers, given a list of /// chunk indices sorted by increasing height and weights for each receiver. pub fn get_multi_drainage( + map_size_lg: MapSizeLg, mstack: &[u32], mrec: &[u8], mwrec: &[Computex8], @@ -66,12 +75,12 @@ pub fn get_multi_drainage( // base_flux to 1.0; this should still work fine because in that case // there's no erosion anyway. let base_area = 1.0; - let mut area = vec![base_area; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut area = vec![base_area; map_size_lg.chunks_len()].into_boxed_slice(); mstack.iter().for_each(|&ij| { let ij = ij as usize; let wrec_ij = &mwrec[ij]; let area_ij = area[ij]; - mrec_downhill(mrec, ij).for_each(|(k, ijr)| { + mrec_downhill(map_size_lg, mrec, ij).for_each(|(k, ijr)| { area[ijr] += area_ij * wrec_ij[k]; }); }); @@ -226,6 +235,7 @@ impl RiverData { /// liberties with the constant factors etc. in order to make it more likely /// that we draw rivers at all. pub fn get_rivers, G: Float + Into>( + map_size_lg: MapSizeLg, newh: &[u32], water_alt: &[F], downhill: &[isize], @@ -236,7 +246,7 @@ pub fn get_rivers, G: Float + Into>( // to build up the derivatives from the top down. Fortunately this // computation seems tractable. - let mut rivers = vec![RiverData::default(); WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut rivers = vec![RiverData::default(); map_size_lg.chunks_len()].into_boxed_slice(); let neighbor_coef = TerrainChunkSize::RECT_SIZE.map(|e| e as f64); // (Roughly) area of a chunk, times minutes per second. // NOTE: Clearly, this should "actually" be 1/60 (or maybe 1/64, if you want to @@ -262,8 +272,8 @@ pub fn get_rivers, G: Float + Into>( return; } let downhill_idx = downhill_idx as usize; - let downhill_pos = uniform_idx_as_vec2(downhill_idx); - let dxy = (downhill_pos - uniform_idx_as_vec2(chunk_idx)).map(|e| e as f64); + let downhill_pos = uniform_idx_as_vec2(map_size_lg, downhill_idx); + let dxy = (downhill_pos - uniform_idx_as_vec2(map_size_lg, chunk_idx)).map(|e| e as f64); let neighbor_dim = neighbor_coef * dxy; // First, we calculate the river's volumetric flow rate. let chunk_drainage = drainage[chunk_idx].into(); @@ -382,10 +392,16 @@ pub fn get_rivers, G: Float + Into>( // This is a pass, so set our flow direction to point to the neighbor pass // rather than downhill. // NOTE: Safe because neighbor_pass_idx >= 0. - (uniform_idx_as_vec2(downhill_idx), river_spline_derivative) + ( + uniform_idx_as_vec2(map_size_lg, downhill_idx), + river_spline_derivative, + ) } else { // Try pointing towards the lake side of the pass. - (uniform_idx_as_vec2(pass_idx), river_spline_derivative) + ( + uniform_idx_as_vec2(map_size_lg, pass_idx), + river_spline_derivative, + ) }; let mut lake = &mut rivers[chunk_idx]; lake.spline_derivative = river_spline_derivative; @@ -427,12 +443,12 @@ pub fn get_rivers, G: Float + Into>( error!( "Our chunk (and downhill, lake, pass, neighbor_pass): {:?} (to {:?}, in {:?} via \ {:?} to {:?}), chunk water alt: {:?}, lake water alt: {:?}", - uniform_idx_as_vec2(chunk_idx), - uniform_idx_as_vec2(downhill_idx), - uniform_idx_as_vec2(lake_idx), - uniform_idx_as_vec2(pass_idx), + uniform_idx_as_vec2(map_size_lg, chunk_idx), + uniform_idx_as_vec2(map_size_lg, downhill_idx), + uniform_idx_as_vec2(map_size_lg, lake_idx), + uniform_idx_as_vec2(map_size_lg, pass_idx), if neighbor_pass_idx >= 0 { - Some(uniform_idx_as_vec2(neighbor_pass_idx as usize)) + Some(uniform_idx_as_vec2(map_size_lg, neighbor_pass_idx as usize)) } else { None }, @@ -550,6 +566,7 @@ pub fn get_rivers, G: Float + Into>( /// TODO: See if allocating in advance is worthwhile. #[allow(clippy::let_and_return)] // TODO: Pending review in #587 fn get_max_slope( + map_size_lg: MapSizeLg, h: &[Alt], rock_strength_nz: &(impl NoiseFn> + Sync), height_scale: impl Fn(usize) -> Alt + Sync, @@ -560,7 +577,7 @@ fn get_max_slope( h.par_iter() .enumerate() .map(|(posi, &z)| { - let wposf = uniform_idx_as_vec2(posi).map(|e| e as f64) + let wposf = uniform_idx_as_vec2(map_size_lg, posi).map(|e| e as f64) * TerrainChunkSize::RECT_SIZE.map(|e| e as f64); let height_scale = height_scale(posi); let wposz = z as f64 / height_scale as f64; @@ -685,6 +702,8 @@ fn get_max_slope( #[allow(clippy::many_single_char_names)] #[allow(clippy::too_many_arguments)] fn erode( + // Underlying map dimensions. + map_size_lg: MapSizeLg, // Height above sea level of topsoil h: &mut [Alt], // Height above sea level of bedrock @@ -733,8 +752,8 @@ fn erode( // at the cost of less interesting erosion behavior (linear vs. nonlinear). let q_ = 1.5; let k_da = 2.5 * k_da_scale(q); - let nx = WORLD_SIZE.x; - let ny = WORLD_SIZE.y; + let nx = usize::from(map_size_lg.chunks().x); + let ny = usize::from(map_size_lg.chunks().y); let dx = TerrainChunkSize::RECT_SIZE.x as f64; let dy = TerrainChunkSize::RECT_SIZE.y as f64; @@ -795,11 +814,17 @@ fn erode( let g_fs_mult_sed = 1.0; let ((dh, newh, maxh, mrec, mstack, mwrec, area), (mut max_slopes, h_t)) = rayon::join( || { - let mut dh = downhill(|posi| h[posi], |posi| is_ocean(posi) && h[posi] <= 0.0); + let mut dh = downhill( + map_size_lg, + |posi| h[posi], + |posi| is_ocean(posi) && h[posi] <= 0.0, + ); debug!("Computed downhill..."); - let (boundary_len, _indirection, newh, maxh) = get_lakes(|posi| h[posi], &mut dh); + let (boundary_len, _indirection, newh, maxh) = + get_lakes(map_size_lg, |posi| h[posi], &mut dh); debug!("Got lakes..."); let (mrec, mstack, mwrec) = get_multi_rec( + map_size_lg, |posi| h[posi], &dh, &newh, @@ -813,16 +838,17 @@ fn erode( debug!("Got multiple receivers..."); // TODO: Figure out how to switch between single-receiver and multi-receiver // drainage, as the former is much less computationally costly. - // let area = get_drainage(&newh, &dh, boundary_len); - let area = get_multi_drainage(&mstack, &mrec, &*mwrec, boundary_len); + // let area = get_drainage(map_size_lg, &newh, &dh, boundary_len); + let area = get_multi_drainage(map_size_lg, &mstack, &mrec, &*mwrec, boundary_len); debug!("Got flux..."); (dh, newh, maxh, mrec, mstack, mwrec, area) }, || { rayon::join( || { - let max_slope = - get_max_slope(h, rock_strength_nz, |posi| height_scale(n_f(posi))); + let max_slope = get_max_slope(map_size_lg, h, rock_strength_nz, |posi| { + height_scale(n_f(posi)) + }); debug!("Got max slopes..."); max_slope }, @@ -870,9 +896,10 @@ fn erode( let m = m_f(posi) as f64; let mwrec_i = &mwrec[posi]; - mrec_downhill(&mrec, posi).for_each(|(kk, posj)| { - let dxy = (uniform_idx_as_vec2(posi) - uniform_idx_as_vec2(posj)) - .map(|e| e as f64); + mrec_downhill(map_size_lg, &mrec, posi).for_each(|(kk, posj)| { + let dxy = (uniform_idx_as_vec2(map_size_lg, posi) + - uniform_idx_as_vec2(map_size_lg, posj)) + .map(|e| e as f64); let neighbor_distance = (neighbor_coef * dxy).magnitude(); let knew = (k * (p as f64 @@ -919,7 +946,7 @@ fn erode( let k_da = k_da * kd_factor; let mwrec_i = &mwrec[posi]; - mrec_downhill(&mrec, posi).for_each(|(kk, posj)| { + mrec_downhill(map_size_lg, &mrec, posi).for_each(|(kk, posj)| { let mwrec_kk = mwrec_i[kk] as f64; #[rustfmt::skip] @@ -965,8 +992,9 @@ fn erode( let k = (mwrec_kk * (uplift_i + max_uplift as f64 * g_i / p as f64)) / (1.0 + k_da * (mwrec_kk * chunk_neutral_area).powf(q)) / max_slope.powf(q_); - let dxy = (uniform_idx_as_vec2(posi) - uniform_idx_as_vec2(posj)) - .map(|e| e as f64); + let dxy = (uniform_idx_as_vec2(map_size_lg, posi) + - uniform_idx_as_vec2(map_size_lg, posj)) + .map(|e| e as f64); let neighbor_distance = (neighbor_coef * dxy).magnitude(); let knew = (k @@ -984,11 +1012,10 @@ fn erode( debug!("Computed stream power factors..."); - let mut lake_water_volume = - vec![0.0 as Compute; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); - let mut elev = vec![0.0 as Compute; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); - let mut h_p = vec![0.0 as Compute; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); - let mut deltah = vec![0.0 as Compute; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut lake_water_volume = vec![0.0 as Compute; map_size_lg.chunks_len()].into_boxed_slice(); + let mut elev = vec![0.0 as Compute; map_size_lg.chunks_len()].into_boxed_slice(); + let mut h_p = vec![0.0 as Compute; map_size_lg.chunks_len()].into_boxed_slice(); + let mut deltah = vec![0.0 as Compute; map_size_lg.chunks_len()].into_boxed_slice(); // calculate the elevation / SPL, including sediment flux let tol1 = 1.0e-4 as Compute * (maxh as Compute + 1.0); @@ -1022,8 +1049,8 @@ fn erode( // Gauss-Seidel iteration - let mut lake_silt = vec![0.0 as Compute; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); - let mut lake_sill = vec![-1isize; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut lake_silt = vec![0.0 as Compute; map_size_lg.chunks_len()].into_boxed_slice(); + let mut lake_sill = vec![-1isize; map_size_lg.chunks_len()].into_boxed_slice(); let mut n_gs_stream_power_law = 0; // NOTE: Increasing this can theoretically sometimes be necessary for @@ -1120,7 +1147,7 @@ fn erode( } } let mwrec_i = &mwrec[posi]; - mrec_downhill(&mrec, posi).for_each(|(k, posj)| { + mrec_downhill(map_size_lg, &mrec, posi).for_each(|(k, posj)| { let stack_posj = mstack_inv[posj]; deltah[stack_posj] += deltah_i * mwrec_i[k]; }); @@ -1269,7 +1296,7 @@ fn erode( if (n - 1.0).abs() <= 1.0e-3 && (q_ - 1.0).abs() <= 1.0e-3 { let mut f = h0; let mut df = 1.0; - mrec_downhill(&mrec, posi).for_each(|(kk, posj)| { + mrec_downhill(map_size_lg, &mrec, posi).for_each(|(kk, posj)| { let posj_stack = mstack_inv[posj]; let h_j = h_stack[posj_stack] as f64; // This can happen in cases where receiver kk is neither uphill of @@ -1295,7 +1322,7 @@ fn erode( let mut errp = 2.0 * tolp; let mut rec_heights = [0.0; 8]; let mut mask = [MaskType::new(false); 8]; - mrec_downhill(&mrec, posi).for_each(|(kk, posj)| { + mrec_downhill(map_size_lg, &mrec, posi).for_each(|(kk, posj)| { let posj_stack = mstack_inv[posj]; let h_j = h_stack[posj_stack]; // NOTE: Fastscape does h_t[posi] + uplift(posi) as f64 >= h_t[posj] @@ -1383,8 +1410,9 @@ fn erode( // of wh_j. new_h_i = wh_j; } else if compute_stats && new_h_i > 0.0 { - let dxy = (uniform_idx_as_vec2(posi) - uniform_idx_as_vec2(posj)) - .map(|e| e as f64); + let dxy = (uniform_idx_as_vec2(map_size_lg, posi) + - uniform_idx_as_vec2(map_size_lg, posj)) + .map(|e| e as f64); let neighbor_distance = (neighbor_coef * dxy).magnitude(); let dz = (new_h_i - wh_j).max(0.0); let mag_slope = dz / neighbor_distance; @@ -1479,7 +1507,9 @@ fn erode( } else { let posj = posj as usize; let h_j = h[posj]; - let dxy = (uniform_idx_as_vec2(posi) - uniform_idx_as_vec2(posj)).map(|e| e as f64); + let dxy = (uniform_idx_as_vec2(map_size_lg, posi) + - uniform_idx_as_vec2(map_size_lg, posj)) + .map(|e| e as f64); let neighbor_distance_squared = (neighbor_coef * dxy).magnitude_squared(); let dh = (h_i - h_j) as f64; // H_i_fact = (h_i - h_j) / (||p_i - p_j||^2 + (h_i - h_j)^2) @@ -1608,7 +1638,9 @@ fn erode( // redistribute uplift to other neighbors. let (posk, h_k) = (posj, h_j); let (posk, h_k) = if h_k < h_j { (posk, h_k) } else { (posj, h_j) }; - let dxy = (uniform_idx_as_vec2(posi) - uniform_idx_as_vec2(posk)).map(|e| e as f64); + let dxy = (uniform_idx_as_vec2(map_size_lg, posi) + - uniform_idx_as_vec2(map_size_lg, posk)) + .map(|e| e as f64); let neighbor_distance = (neighbor_coef * dxy).magnitude(); let dz = (new_h_i - h_k).max(0.0); let mag_slope = dz / neighbor_distance; @@ -1706,6 +1738,7 @@ fn erode( /// /// See https://github.com/mewo2/terrain/blob/master/terrain.js pub fn fill_sinks( + map_size_lg: MapSizeLg, h: impl Fn(usize) -> F + Sync, is_ocean: impl Fn(usize) -> bool + Sync, ) -> Box<[F]> { @@ -1713,7 +1746,7 @@ pub fn fill_sinks( // but doesn't change altitudes. let epsilon = F::zero(); let infinity = F::infinity(); - let range = 0..WORLD_SIZE.x * WORLD_SIZE.y; + let range = 0..map_size_lg.chunks_len(); let oldh = range .into_par_iter() .map(|posi| h(posi)) @@ -1742,7 +1775,7 @@ pub fn fill_sinks( if nh == oh { return; } - for nposi in neighbors(posi) { + for nposi in neighbors(map_size_lg, posi) { let onbh = newh[nposi]; let nbh = onbh + epsilon; // If there is even one path downhill from this node's original height, fix @@ -1793,6 +1826,7 @@ pub fn fill_sinks( /// indirection vector. #[allow(clippy::filter_next)] // TODO: Pending review in #587 pub fn get_lakes( + map_size_lg: MapSizeLg, h: impl Fn(usize) -> F, downhill: &mut [isize], ) -> (usize, Box<[i32]>, Box<[u32]>, F) { @@ -1817,7 +1851,7 @@ pub fn get_lakes( // node, so we should access that entry and increment it, then set our own // entry to it. let mut boundary = Vec::with_capacity(downhill.len()); - let mut indirection = vec![/*-1i32*/0i32; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut indirection = vec![/*-1i32*/0i32; map_size_lg.chunks_len()].into_boxed_slice(); let mut newh = Vec::with_capacity(downhill.len()); @@ -1855,7 +1889,7 @@ pub fn get_lakes( let mut cur = start; while cur < newh.len() { let node = newh[cur as usize]; - uphill(downhill, node as usize).for_each(|child| { + uphill(map_size_lg, downhill, node as usize).for_each(|child| { // lake_idx is the index of our lake root. indirection[child] = chunk_idx as i32; indirection_[child] = indirection_idx; @@ -1896,7 +1930,7 @@ pub fn get_lakes( // our own lake's entry list. If the maximum of the heights we get out // from this process is greater than the maximum of this chunk and its // neighbor chunk, we switch to this new edge. - neighbors(chunk_idx).for_each(|neighbor_idx| { + neighbors(map_size_lg, chunk_idx).for_each(|neighbor_idx| { let neighbor_height = h(neighbor_idx); let neighbor_lake_idx_ = indirection_[neighbor_idx]; let neighbor_lake_idx = neighbor_lake_idx_ as usize; @@ -1962,24 +1996,33 @@ pub fn get_lakes( "For edge {:?} between lakes {:?}, couldn't find partner for pass \ {:?}. Should never happen; maybe forgot to set both edges?", ( - (chunk_idx, uniform_idx_as_vec2(chunk_idx as usize)), - (neighbor_idx, uniform_idx_as_vec2(neighbor_idx as usize)) + ( + chunk_idx, + uniform_idx_as_vec2(map_size_lg, chunk_idx as usize) + ), + ( + neighbor_idx, + uniform_idx_as_vec2(map_size_lg, neighbor_idx as usize) + ) ), ( ( lake_chunk_idx, - uniform_idx_as_vec2(lake_chunk_idx as usize), + uniform_idx_as_vec2(map_size_lg, lake_chunk_idx as usize), lake_idx_ ), ( neighbor_lake_chunk_idx, - uniform_idx_as_vec2(neighbor_lake_chunk_idx as usize), + uniform_idx_as_vec2( + map_size_lg, + neighbor_lake_chunk_idx as usize + ), neighbor_lake_idx_ ) ), ( - (pass.0, uniform_idx_as_vec2(pass.0 as usize)), - (pass.1, uniform_idx_as_vec2(pass.1 as usize)) + (pass.0, uniform_idx_as_vec2(map_size_lg, pass.0 as usize)), + (pass.1, uniform_idx_as_vec2(map_size_lg, pass.1 as usize)) ), ); } @@ -2068,7 +2111,7 @@ pub fn get_lakes( downhill[lake_chunk_idx] = neighbor_idx as isize; // Also set the indirection of the lake bottom to the negation of the // lake side of the chosen pass (chunk_idx). - // NOTE: This can't overflow i32 because WORLD_SIZE.x * WORLD_SIZE.y should fit + // NOTE: This can't overflow i32 because map_size_lg.chunks_len() should fit // in an i32. indirection[lake_chunk_idx] = -(chunk_idx as i32); // Add this edge to the sorted list. @@ -2114,7 +2157,7 @@ pub fn get_lakes( InQueue, WithRcv, } - let mut tag = vec![Tag::UnParsed; WORLD_SIZE.x * WORLD_SIZE.y]; + let mut tag = vec![Tag::UnParsed; map_size_lg.chunks_len()]; // TODO: Combine with adding to vector. let mut filling_queue = Vec::with_capacity(downhill.len()); @@ -2182,17 +2225,17 @@ pub fn get_lakes( tag[neighbor_pass_idx] = Tag::WithRcv; tag[pass_idx] = Tag::InQueue; - let outflow_coords = uniform_idx_as_vec2(neighbor_pass_idx); + let outflow_coords = uniform_idx_as_vec2(map_size_lg, neighbor_pass_idx); let elev = h(neighbor_pass_idx).max(h(pass_idx)); while let Some(node) = filling_queue.pop() { - let coords = uniform_idx_as_vec2(node); + let coords = uniform_idx_as_vec2(map_size_lg, node); let mut rcv = -1; let mut rcv_cost = -f64::INFINITY; /*f64::EPSILON;*/ let outflow_distance = (outflow_coords - coords).map(|e| e as f64); - neighbors(node).for_each(|ineighbor| { + neighbors(map_size_lg, node).for_each(|ineighbor| { if indirection[ineighbor] != chunk_idx as i32 && ineighbor != chunk_idx && ineighbor != neighbor_pass_idx @@ -2200,7 +2243,8 @@ pub fn get_lakes( { return; } - let dxy = (uniform_idx_as_vec2(ineighbor) - coords).map(|e| e as f64); + let dxy = (uniform_idx_as_vec2(map_size_lg, ineighbor) - coords) + .map(|e| e as f64); let neighbor_distance = /*neighbor_coef * */dxy; let tag = &mut tag[ineighbor]; match *tag { @@ -2242,7 +2286,7 @@ pub fn get_lakes( let mut cur = start; let mut node = first_idx; loop { - uphill(downhill, node as usize).for_each(|child| { + uphill(map_size_lg, downhill, node as usize).for_each(|child| { // lake_idx is the index of our lake root. // Check to make sure child (flowing into us) is in the same lake. if indirection[child] == chunk_idx as i32 || child == chunk_idx { @@ -2263,10 +2307,11 @@ pub fn get_lakes( /// Iterate through set neighbors of multi-receiver flow. pub fn mrec_downhill<'a>( + map_size_lg: MapSizeLg, mrec: &'a [u8], posi: usize, ) -> impl Clone + Iterator + 'a { - let pos = uniform_idx_as_vec2(posi); + let pos = uniform_idx_as_vec2(map_size_lg, posi); let mrec_i = mrec[posi]; NEIGHBOR_DELTA .iter() @@ -2275,13 +2320,14 @@ pub fn mrec_downhill<'a>( .map(move |(k, &(x, y))| { ( k, - vec2_as_uniform_idx(Vec2::new(pos.x + x as i32, pos.y + y as i32)), + vec2_as_uniform_idx(map_size_lg, Vec2::new(pos.x + x as i32, pos.y + y as i32)), ) }) } /// Algorithm for computing multi-receiver flow. /// +/// * `map_size_lg`: Size of the underlying map. /// * `h`: altitude /// * `downhill`: single receiver /// * `newh`: single receiver stack @@ -2299,6 +2345,7 @@ pub fn mrec_downhill<'a>( #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] // TODO: Pending review in #587 pub fn get_multi_rec>( + map_size_lg: MapSizeLg, h: impl Fn(usize) -> F + Sync, downhill: &[isize], newh: &[u32], @@ -2379,18 +2426,15 @@ pub fn get_multi_rec>( let mut mrec_ij = 0u8; let mut ndon_ij = 0u8; let neighbor_iter = |posi| { - let pos = uniform_idx_as_vec2(posi); + let pos = uniform_idx_as_vec2(map_size_lg, posi); NEIGHBOR_DELTA .iter() .map(move |&(x, y)| Vec2::new(pos.x + x, pos.y + y)) .enumerate() .filter(move |&(_, pos)| { - pos.x >= 0 - && pos.y >= 0 - && pos.x < WORLD_SIZE.x as i32 - && pos.y < WORLD_SIZE.y as i32 + pos.x >= 0 && pos.y >= 0 && pos.x < nx as i32 && pos.y < ny as i32 }) - .map(move |(k, pos)| (k, vec2_as_uniform_idx(pos))) + .map(move |(k, pos)| (k, vec2_as_uniform_idx(map_size_lg, pos))) }; neighbor_iter(ij).for_each(|(k, ijk)| { @@ -2417,9 +2461,10 @@ pub fn get_multi_rec>( let mut sumweight = czero; let mut wrec = [czero; 8]; let mut nrec = 0; - mrec_downhill(&mrec, ij).for_each(|(k, ijk)| { - let lrec_ijk = ((uniform_idx_as_vec2(ijk) - uniform_idx_as_vec2(ij)) - .map(|e| e as Compute) + mrec_downhill(map_size_lg, &mrec, ij).for_each(|(k, ijk)| { + let lrec_ijk = ((uniform_idx_as_vec2(map_size_lg, ijk) + - uniform_idx_as_vec2(map_size_lg, ij)) + .map(|e| e as Compute) * dxdy) .magnitude(); let wrec_ijk = (wh[ij] - wh[ijk]).into() / lrec_ijk; @@ -2464,7 +2509,7 @@ pub fn get_multi_rec>( while let Some(ijn) = parse.pop() { // we add the node to the stack stack.push(ijn as u32); - mrec_downhill(&mrec, ijn).for_each(|(_, ijr)| { + mrec_downhill(map_size_lg, &mrec, ijn).for_each(|(_, ijr)| { let (_, ref mut vis_ijr) = don_vis[ijr]; if *vis_ijr >= 1 { *vis_ijr -= 1; @@ -2492,6 +2537,7 @@ pub fn get_multi_rec>( #[allow(clippy::many_single_char_names)] #[allow(clippy::too_many_arguments)] pub fn do_erosion( + map_size_lg: MapSizeLg, _max_uplift: f32, n_steps: usize, seed: &RandomField, @@ -2513,59 +2559,59 @@ pub fn do_erosion( k_da_scale: impl Fn(f64) -> f64, ) -> (Box<[Alt]>, Box<[Alt]> /* , Box<[Alt]> */) { debug!("Initializing erosion arrays..."); - let oldh_ = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let oldh_ = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| oldh(posi) as Alt) .collect::>() .into_boxed_slice(); // Topographic basement (The height of bedrock, not including sediment). - let mut b = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let mut b = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| oldb(posi) as Alt) .collect::>() .into_boxed_slice(); // Stream power law slope exponent--link between channel slope and erosion rate. - let n = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let n = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| n(posi)) .collect::>() .into_boxed_slice(); // Stream power law concavity index (θ = m/n), turned into an exponent on // drainage (which is a proxy for discharge according to Hack's Law). - let m = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let m = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| theta(posi) * n[posi]) .collect::>() .into_boxed_slice(); // Stream power law erodability constant for fluvial erosion (bedrock) - let kf = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let kf = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| kf(posi)) .collect::>() .into_boxed_slice(); // Stream power law erodability constant for hillslope diffusion (bedrock) - let kd = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let kd = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| kd(posi)) .collect::>() .into_boxed_slice(); // Deposition coefficient - let g = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let g = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| g(posi)) .collect::>() .into_boxed_slice(); - let epsilon_0 = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let epsilon_0 = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| epsilon_0(posi)) .collect::>() .into_boxed_slice(); - let alpha = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let alpha = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| alpha(posi)) .collect::>() .into_boxed_slice(); - let mut wh = vec![0.0; WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut wh = vec![0.0; map_size_lg.chunks_len()].into_boxed_slice(); // TODO: Don't do this, maybe? // (To elaborate, maybe we should have varying uplift or compute it some other // way). @@ -2613,6 +2659,7 @@ pub fn do_erosion( (0..n_steps).for_each(|i| { debug!("Erosion iteration #{:?}", i); erode( + map_size_lg, &mut h, &mut b, &mut wh, diff --git a/world/src/sim/map.rs b/world/src/sim/map.rs index bd69fd384d..6f1fc7ec1a 100644 --- a/world/src/sim/map.rs +++ b/world/src/sim/map.rs @@ -1,712 +1,264 @@ use crate::{ - column::{quadratic_nearest_point, river_spline_coeffs, ColumnSample}, - sim::{ - neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, Alt, RiverKind, WorldSim, - NEIGHBOR_DELTA, WORLD_SIZE, - }, + column::ColumnSample, + sim::{RiverKind, WorldSim}, CONFIG, }; -use common::{terrain::TerrainChunkSize, vol::RectVolSize}; -use std::{f32, f64, iter}; +use common::{ + terrain::{ + map::{Connection, ConnectionKind, MapConfig, MapSample}, + vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA, + }, + vol::RectVolSize, +}; +use std::{f32, f64}; use vek::*; -pub struct MapConfig<'a> { - /// Dimensions of the window being written to. Defaults to WORLD_SIZE. - pub dimensions: Vec2, - /// x, y, and z of top left of map (defaults to (0.0, 0.0, - /// CONFIG.sea_level)). - pub focus: Vec3, - /// Altitude is divided by gain and clamped to [0, 1]; thus, decreasing gain - /// makes smaller differences in altitude appear larger. - /// - /// Defaults to CONFIG.mountain_scale. - pub gain: f32, - /// lgain is used for shading purposes and refers to how much impact a - /// change in the z direction has on the perceived slope relative to the - /// same change in x and y. - /// - /// Defaults to TerrainChunkSize::RECT_SIZE.x. - pub lgain: f64, - /// Scale is like gain, but for x and y rather than z. - /// - /// Defaults to WORLD_SIZE.x / dimensions.x (NOTE: fractional, not integer, - /// division!). - pub scale: f64, - /// Vector that indicates which direction light is coming from, if shading - /// is turned on. - /// - /// Right-handed coordinate system: light is going left, down, and - /// "backwards" (i.e. on the map, where we translate the y coordinate on - /// the world map to z in the coordinate system, the light comes from -y - /// on the map and points towards +y on the map). In a right - /// handed coordinate system, the "camera" points towards -z, so positive z - /// is backwards "into" the camera. - /// - /// "In world space the x-axis will be pointing east, the y-axis up and the - /// z-axis will be pointing south" - /// - /// Defaults to (-0.8, -1.0, 0.3). - pub light_direction: Vec3, - /// If Some, uses the provided horizon map. - /// - /// Defaults to None. - pub horizons: Option<&'a [(Vec, Vec); 2]>, - /// If Some, uses the provided column samples to determine surface color. - /// - /// Defaults to None. - pub samples: Option<&'a [Option>]>, - /// If true, only the basement (bedrock) is used for altitude; otherwise, - /// the surface is used. - /// - /// Defaults to false. - pub is_basement: bool, - /// If true, water is rendered; otherwise, the surface without water is - /// rendered, even if it is underwater. - /// - /// Defaults to true. - pub is_water: bool, - /// If true, 3D lighting and shading are turned on. Otherwise, a plain - /// altitude map is used. - /// - /// Defaults to true. - pub is_shaded: bool, - /// If true, the red component of the image is also used for temperature - /// (redder is hotter). Defaults to false. - pub is_temperature: bool, - /// If true, the blue component of the image is also used for humidity - /// (bluer is wetter). - /// - /// Defaults to false. - pub is_humidity: bool, - /// Record debug information. - /// - /// Defaults to false. - pub is_debug: bool, -} +/// A sample function that grabs the connections at a chunk. +/// +/// Currently this just supports rivers, but ideally it can be extended past +/// that. +/// +/// A sample function that grabs surface altitude at a column. +/// (correctly reflecting settings like is_basement and is_water). +/// +/// The altitude produced by this function at a column corresponding to a +/// particular chunk should be identical to the altitude produced by +/// sample_pos at that chunk. +/// +/// You should generally pass a closure over this function into generate +/// when constructing a map for the first time. +/// However, if repeated construction is needed, or alternate base colors +/// are to be used for some reason, one should pass a custom function to +/// generate instead (e.g. one that just looks up the height in a cached +/// array). +pub fn sample_wpos(config: &MapConfig, sampler: &WorldSim, wpos: Vec2) -> f32 { + let MapConfig { + focus, + gain, -pub const QUADRANTS: usize = 4; + is_basement, + is_water, + .. + } = *config; -pub struct MapDebug { - pub quads: [[u32; QUADRANTS]; QUADRANTS], - pub rivers: u32, - pub lakes: u32, - pub oceans: u32, -} - -impl<'a> Default for MapConfig<'a> { - fn default() -> Self { - let dimensions = WORLD_SIZE; - Self { - dimensions, - focus: Vec3::new(0.0, 0.0, CONFIG.sea_level as f64), - gain: CONFIG.mountain_scale, - lgain: TerrainChunkSize::RECT_SIZE.x as f64, - scale: WORLD_SIZE.x as f64 / dimensions.x as f64, - light_direction: Vec3::new(-1.2, -1.0, 0.8), - horizons: None, - samples: None, - - is_basement: false, - is_water: true, - is_shaded: true, - is_temperature: false, - is_humidity: false, - is_debug: false, - } - } -} - -/// Connection kind (per edge). Currently just supports rivers, but may be -/// extended to support paths or at least one other kind of connection. -#[derive(Clone, Copy, Debug)] -pub enum ConnectionKind { - /// Connection forms a visible river. - River, -} - -/// Map connection (per edge). -#[derive(Clone, Copy, Debug)] -pub struct Connection { - /// The kind of connection this is (e.g. river or path). - pub kind: ConnectionKind, - /// Assumed to be the "b" part of a 2d quadratic function. - pub spline_derivative: Vec2, - /// Width of the connection. - pub width: f32, -} - -/// Per-chunk data the map needs to be able to sample in order to correctly -/// render. -#[derive(Clone, Debug)] -pub struct MapSample { - /// the base RGB color for a particular map pixel using the current settings - /// (i.e. the color *without* lighting). - pub rgb: Rgb, - /// Surface altitude information - /// (correctly reflecting settings like is_basement and is_water) - pub alt: f64, - /// Downhill chunk (may not be meaningful on ocean tiles, or at least edge - /// tiles) - pub downhill_wpos: Vec2, - /// Connection information about any connections to/from this chunk (e.g. - /// rivers). - /// - /// Connections at each index correspond to the same index in - /// NEIGHBOR_DELTA. - pub connections: Option<[Option; 8]>, -} - -impl<'a> MapConfig<'a> { - /// A sample function that grabs the connections at a chunk. - /// - /// Currently this just supports rivers, but ideally it can be extended past - /// that. - /// - /// A sample function that grabs surface altitude at a column. - /// (correctly reflecting settings like is_basement and is_water). - /// - /// The altitude produced by this function at a column corresponding to a - /// particular chunk should be identical to the altitude produced by - /// sample_pos at that chunk. - /// - /// You should generally pass a closure over this function into generate - /// when constructing a map for the first time. - /// However, if repeated construction is needed, or alternate base colors - /// are to be used for some reason, one should pass a custom function to - /// generate instead (e.g. one that just looks up the height in a cached - /// array). - pub fn sample_wpos(&self, sampler: &WorldSim, wpos: Vec2) -> f32 { - let MapConfig { - focus, - gain, - - is_basement, - is_water, - .. - } = *self; - - (sampler - .get_wpos(wpos) - .map(|s| { - if is_basement { s.basement } else { s.alt }.max(if is_water { - s.water_alt - } else { - -f32::INFINITY - }) + (sampler + .get_wpos(wpos) + .map(|s| { + if is_basement { s.basement } else { s.alt }.max(if is_water { + s.water_alt + } else { + -f32::INFINITY }) - .unwrap_or(CONFIG.sea_level) - - focus.z as f32) - / gain as f32 - } + }) + .unwrap_or(CONFIG.sea_level) + - focus.z as f32) + / gain as f32 +} - /// Samples a MapSample at a chunk. - /// - /// You should generally pass a closure over this function into generate - /// when constructing a map for the first time. - /// However, if repeated construction is needed, or alternate base colors - /// are to be used for some reason, one should pass a custom function to - /// generate instead (e.g. one that just looks up the color in a cached - /// array). - pub fn sample_pos(&self, sampler: &WorldSim, pos: Vec2) -> MapSample { - let MapConfig { - focus, - gain, - samples, +/// Samples a MapSample at a chunk. +/// +/// You should generally pass a closure over this function into generate +/// when constructing a map for the first time. +/// However, if repeated construction is needed, or alternate base colors +/// are to be used for some reason, one should pass a custom function to +/// generate instead (e.g. one that just looks up the color in a cached +/// array). +pub fn sample_pos( + config: &MapConfig, + sampler: &WorldSim, + samples: Option<&[Option]>, + pos: Vec2, +) -> MapSample { + let map_size_lg = config.map_size_lg(); + let MapConfig { + focus, + gain, - is_basement, - is_water, - is_shaded, - is_temperature, - is_humidity, - // is_debug, - .. - } = *self; + is_basement, + is_water, + is_shaded, + is_temperature, + is_humidity, + // is_debug, + .. + } = *config; - let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64; + let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64; - let ( - chunk_idx, - alt, - basement, - water_alt, - humidity, - temperature, - downhill, - river_kind, - spline_derivative, - is_path, - near_site, - ) = sampler - .get(pos) - .map(|sample| { - ( - Some(vec2_as_uniform_idx(pos)), - sample.alt, - sample.basement, - sample.water_alt, - sample.humidity, - sample.temp, - sample.downhill, - sample.river.river_kind, - sample.river.spline_derivative, - sample.path.is_path(), - sample.sites.iter().any(|site| { - site.get_origin() - .distance_squared(pos * TerrainChunkSize::RECT_SIZE.x as i32) - < 64i32.pow(2) - }), - ) - }) - .unwrap_or(( - None, - CONFIG.sea_level, - CONFIG.sea_level, - CONFIG.sea_level, - 0.0, - 0.0, - None, - None, - Vec2::zero(), - false, - false, - )); + let ( + chunk_idx, + alt, + basement, + water_alt, + humidity, + temperature, + downhill, + river_kind, + spline_derivative, + is_path, + near_site, + ) = sampler + .get(pos) + .map(|sample| { + ( + Some(vec2_as_uniform_idx(map_size_lg, pos)), + sample.alt, + sample.basement, + sample.water_alt, + sample.humidity, + sample.temp, + sample.downhill, + sample.river.river_kind, + sample.river.spline_derivative, + sample.path.is_path(), + sample.sites.iter().any(|site| { + site.get_origin() + .distance_squared(pos * TerrainChunkSize::RECT_SIZE.x as i32) + < 64i32.pow(2) + }), + ) + }) + .unwrap_or(( + None, + CONFIG.sea_level, + CONFIG.sea_level, + CONFIG.sea_level, + 0.0, + 0.0, + None, + None, + Vec2::zero(), + false, + false, + )); - let humidity = humidity.min(1.0).max(0.0); - let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5; - let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); - let column_rgb = samples - .and_then(|samples| { - chunk_idx - .and_then(|chunk_idx| samples.get(chunk_idx)) - .map(Option::as_ref) - .flatten() - }) - .map(|sample| { - // TODO: Eliminate the redundancy between this and the block renderer. - let alt = sample.alt; - let basement = sample.basement; - let grass_depth = (1.5 + 2.0 * sample.chaos).min(alt - basement); - let wposz = if is_basement { basement } else { alt }; - if is_basement && wposz < alt - grass_depth { - Lerp::lerp( - sample.sub_surface_color, - sample.stone_col.map(|e| e as f32 / 255.0), - (alt - grass_depth - wposz as f32) * 0.15, - ) - .map(|e| e as f64) - } else { - Lerp::lerp( - sample.sub_surface_color, - sample.surface_color, - ((wposz as f32 - (alt - grass_depth)) / grass_depth).powf(0.5), - ) - .map(|e| e as f64) - } - }); - - let downhill_wpos = downhill - .map(|downhill_pos| downhill_pos) - .unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32)); - let alt = if is_basement { basement } else { alt }; - - let true_water_alt = (alt.max(water_alt) as f64 - focus.z) / gain as f64; - let true_alt = (alt as f64 - focus.z) / gain as f64; - let water_depth = (true_water_alt - true_alt).min(1.0).max(0.0); - let alt = true_alt.min(1.0).max(0.0); - - let water_color_factor = 2.0; - let g_water = 32.0 * water_color_factor; - let b_water = 64.0 * water_color_factor; - let default_rgb = Rgb::new( - if is_shaded || is_temperature { - 1.0 - } else { - 0.0 - }, - if is_shaded { 1.0 } else { alt }, - if is_shaded || is_humidity { 1.0 } else { 0.0 }, - ); - let column_rgb = column_rgb.unwrap_or(default_rgb); - let mut connections = [None; 8]; - let mut has_connections = false; - // TODO: Support non-river connections. - // TODO: Support multiple connections. - let river_width = river_kind.map(|river| match river { - RiverKind::River { cross_section } => cross_section.x, - RiverKind::Lake { .. } | RiverKind::Ocean => TerrainChunkSize::RECT_SIZE.x as f32, - }); - if let (Some(river_width), true) = (river_width, is_water) { - let downhill_pos = downhill_wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); - NEIGHBOR_DELTA - .iter() - .zip((&mut connections).iter_mut()) - .filter(|&(&offset, _)| downhill_pos - pos == Vec2::from(offset)) - .for_each(|(_, connection)| { - has_connections = true; - *connection = Some(Connection { - kind: ConnectionKind::River, - spline_derivative, - width: river_width, - }); - }); - }; - let rgb = match (river_kind, (is_water, true_alt >= true_sea_level)) { - (_, (false, _)) | (None, (_, true)) | (Some(RiverKind::River { .. }), _) => { - let (r, g, b) = ( - (column_rgb.r - * if is_temperature { - temperature as f64 - } else { - column_rgb.r - }) - .sqrt(), - column_rgb.g, - (column_rgb.b - * if is_humidity { - humidity as f64 - } else { - column_rgb.b - }) - .sqrt(), - ); - Rgb::new((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8) - }, - (None, _) | (Some(RiverKind::Lake { .. }), _) | (Some(RiverKind::Ocean), _) => { - Rgb::new( - 0, - ((g_water - water_depth * g_water) * 1.0) as u8, - ((b_water - water_depth * b_water) * 1.0) as u8, - ) - }, - }; - // TODO: Make principled. - let rgb = if near_site { - Rgb::new(0x57, 0x39, 0x33) - } else if is_path { - Rgb::new(0x37, 0x29, 0x23) - } else { - rgb - }; - - MapSample { - rgb, - alt: if is_water { - true_alt.max(true_water_alt) - } else { - true_alt - }, - downhill_wpos, - connections: if has_connections { - Some(connections) - } else { - None - }, - } - } - - /// Generates a map image using the specified settings. Note that it will - /// write from left to write from (0, 0) to dimensions - 1, inclusive, - /// with 4 1-byte color components provided as (r, g, b, a). It is up - /// to the caller to provide a function that translates this information - /// into the correct format for a buffer and writes to it. - /// - /// sample_pos is a function that, given a chunk position, returns enough - /// information about the chunk to attempt to render it on the map. - /// When in doubt, try using `MapConfig::sample_pos` for this. - /// - /// sample_wpos is a simple function that, given a *column* position, - /// returns the approximate altitude at that column. When in doubt, try - /// using `MapConfig::sample_wpos` for this. - #[allow(clippy::if_same_then_else)] // TODO: Pending review in #587 - #[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587 - #[allow(clippy::many_single_char_names)] - pub fn generate( - &self, - sample_pos: impl Fn(Vec2) -> MapSample, - sample_wpos: impl Fn(Vec2) -> f32, - // sampler: &WorldSim, - mut write_pixel: impl FnMut(Vec2, (u8, u8, u8, u8)), - ) -> MapDebug { - let MapConfig { - dimensions, - focus, - gain, - lgain, - scale, - light_direction, - horizons, - - is_shaded, - // is_debug, - .. - } = *self; - - let light_direction = Vec3::new( - light_direction.x, - light_direction.y, - 0.0, // we currently ignore light_direction.z. - ); - let light_shadow_dir = if light_direction.x >= 0.0 { 0 } else { 1 }; - let horizon_map = horizons.map(|horizons| &horizons[light_shadow_dir]); - let light = light_direction.normalized(); - let /*mut */quads = [[0u32; QUADRANTS]; QUADRANTS]; - let /*mut */rivers = 0u32; - let /*mut */lakes = 0u32; - let /*mut */oceans = 0u32; - - let focus_rect = Vec2::from(focus); - - let chunk_size = TerrainChunkSize::RECT_SIZE.map(|e| e as f64); - - (0..dimensions.y * dimensions.x).for_each(|chunk_idx| { - let i = chunk_idx % dimensions.x as usize; - let j = chunk_idx / dimensions.x as usize; - - let wposf = focus_rect + Vec2::new(i as f64, j as f64) * scale; - let pos = wposf.map(|e: f64| e as i32); - let wposf = wposf * chunk_size; - - let chunk_idx = if pos.reduce_partial_min() >= 0 - && pos.x < WORLD_SIZE.x as i32 - && pos.y < WORLD_SIZE.y as i32 - { - Some(vec2_as_uniform_idx(pos)) - } else { - None - }; - - let MapSample { - rgb, - alt, - downhill_wpos, - .. - } = sample_pos(pos); - - let alt = alt as f32; - let wposi = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); - let mut rgb = rgb.map(|e| e as f64 / 255.0); - - // Material properties: - // - // For each material in the scene, - // k_s = (RGB) specular reflection constant - let mut k_s = Rgb::new(1.0, 1.0, 1.0); - // k_d = (RGB) diffuse reflection constant - let mut k_d = rgb; - // k_a = (RGB) ambient reflection constant - let mut k_a = rgb; - // α = (per-material) shininess constant - let mut alpha = 4.0; // 4.0; - - // Compute connections - let mut has_river = false; - // NOTE: consider replacing neighbors with local_cells, since it is more - // accurate (though I'm not sure if it can matter for these - // purposes). + let humidity = humidity.min(1.0).max(0.0); + let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5; + let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let column_rgb = samples + .and_then(|samples| { chunk_idx - .map(|chunk_idx| neighbors(chunk_idx).chain(iter::once(chunk_idx))) - .into_iter() + .and_then(|chunk_idx| samples.get(chunk_idx)) + .map(Option::as_ref) .flatten() - .for_each(|neighbor_posi| { - let neighbor_pos = uniform_idx_as_vec2(neighbor_posi); - let neighbor_wpos = neighbor_pos.map(|e| e as f64) * chunk_size; - let MapSample { connections, .. } = sample_pos(neighbor_pos); - NEIGHBOR_DELTA - .iter() - .zip( - connections - .as_ref() - .map(|e| e.iter()) - .into_iter() - .flatten() - .into_iter(), - ) - .for_each(|(&delta, connection)| { - let connection = if let Some(connection) = connection { - connection - } else { - return; - }; - let downhill_wpos = neighbor_wpos - + Vec2::from(delta).map(|e: i32| e as f64) * chunk_size; - let coeffs = river_spline_coeffs( - neighbor_wpos, - connection.spline_derivative, - downhill_wpos, - ); - let (_t, _pt, dist) = if let Some((t, pt, dist)) = - quadratic_nearest_point(&coeffs, wposf) - { - (t, pt, dist) - } else { - let ndist = wposf.distance_squared(neighbor_wpos); - let ddist = wposf.distance_squared(downhill_wpos); - if ndist <= ddist { - (0.0, neighbor_wpos, ndist) - } else { - (1.0, downhill_wpos, ddist) - } - }; - let connection_dist = - (dist.sqrt() - (connection.width as f64 * 0.5).max(1.0)).max(0.0); - if connection_dist == 0.0 { - match connection.kind { - ConnectionKind::River => { - has_river = true; - }, - } - } - }); - }); - - // Color in connectins. - let water_color_factor = 2.0; - let g_water = 32.0 * water_color_factor; - let b_water = 64.0 * water_color_factor; - if has_river { - let water_rgb = Rgb::new(0, ((g_water) * 1.0) as u8, ((b_water) * 1.0) as u8) - .map(|e| e as f64 / 255.0); - rgb = water_rgb; - k_s = Rgb::new(1.0, 1.0, 1.0); - k_d = water_rgb; - k_a = water_rgb; - alpha = 0.255; - } - - let downhill_alt = sample_wpos(downhill_wpos); - let cross_pos = wposi - + ((downhill_wpos - wposi) - .map(|e| e as f32) - .rotated_z(f32::consts::FRAC_PI_2) - .map(|e| e as i32)); - let cross_alt = sample_wpos(cross_pos); - // Pointing downhill, forward - // (index--note that (0,0,1) is backward right-handed) - let forward_vec = Vec3::new( - (downhill_wpos.x - wposi.x) as f64, - ((downhill_alt - alt) * gain) as f64 * lgain, - (downhill_wpos.y - wposi.y) as f64, - ); - // Pointing 90 degrees left (in horizontal xy) of downhill, up - // (middle--note that (1,0,0), 90 degrees CCW backward, is right right-handed) - let up_vec = Vec3::new( - (cross_pos.x - wposi.x) as f64, - ((cross_alt - alt) * gain) as f64 * lgain, - (cross_pos.y - wposi.y) as f64, - ); - // let surface_normal = Vec3::new(lgain * (f.y * u.z - f.z * u.y), -(f.x * u.z - - // f.z * u.x), lgain * (f.x * u.y - f.y * u.x)).normalized(); - // Then cross points "to the right" (upwards) on a right-handed coordinate - // system. (right-handed coordinate system means (0, 0, 1.0) is - // "forward" into the screen). - let surface_normal = forward_vec.cross(up_vec).normalized(); - - // TODO: Figure out if we can reimplement debugging. - /* if is_debug { - let quad = - |x: f32| ((x as f64 * QUADRANTS as f64).floor() as usize).min(QUADRANTS - 1); - if river_kind.is_none() || humidity != 0.0 { - quads[quad(humidity)][quad(temperature)] += 1; - } - match river_kind { - Some(RiverKind::River { .. }) => { - rivers += 1; - }, - Some(RiverKind::Lake { .. }) => { - lakes += 1; - }, - Some(RiverKind::Ocean { .. }) => { - oceans += 1; - }, - None => {}, - } - } */ - - let shade_frac = horizon_map - .and_then(|(angles, heights)| { - chunk_idx - .and_then(|chunk_idx| angles.get(chunk_idx)) - .map(|&e| (e as f64, heights)) - }) - .and_then(|(e, heights)| { - chunk_idx - .and_then(|chunk_idx| heights.get(chunk_idx)) - .map(|&f| (e, f as f64)) - }) - .map(|(angle, height)| { - let w = 0.1; - let height = (height - alt as Alt * gain as Alt).max(0.0); - if angle != 0.0 && light_direction.x != 0.0 && height != 0.0 { - let deltax = height / angle; - let lighty = (light_direction.y / light_direction.x * deltax).abs(); - let deltay = lighty - height; - let s = (deltay / deltax / w).min(1.0).max(0.0); - // Smoothstep - s * s * (3.0 - 2.0 * s) - } else { - 1.0 - } - }) - .unwrap_or(1.0); - - let rgb = if is_shaded { - // Phong reflection model with shadows: - // - // I_p = k_a i_a + shadow * Σ {m ∈ lights} (k_d (L_m ⋅ N) i_m,d + k_s (R_m ⋅ - // V)^α i_m,s) - // - // where for the whole scene, - // i_a = (RGB) intensity of ambient lighting component - let i_a = Rgb::new(0.1, 0.1, 0.1); - // V = direction pointing towards the viewer (e.g. virtual camera). - let v = Vec3::new(0.0, 0.0, -1.0).normalized(); - // let v = Vec3::new(0.0, -1.0, 0.0).normalized(); - // - // for each light m, - // i_m,d = (RGB) intensity of diffuse component of light source m - let i_m_d = Rgb::new(1.0, 1.0, 1.0); - // i_m,s = (RGB) intensity of specular component of light source m - let i_m_s = Rgb::new(0.45, 0.45, 0.45); - // let i_m_s = Rgb::new(0.45, 0.45, 0.45); - - // for each light m and point p, - // L_m = (normalized) direction vector from point on surface to light source m - let l_m = light; - // N = (normalized) normal at this point on the surface, - let n = surface_normal; - // R_m = (normalized) direction a perfectly reflected ray of light from m would - // take from point p = 2(L_m ⋅ N)N - L_m - let r_m = (-l_m).reflected(n); // 2 * (l_m.dot(n)) * n - l_m; - // - // and for each point p in the scene, - // shadow = computed shadow factor at point p - // FIXME: Should really just be shade_frac, but with only ambient light we lose - // all local lighting detail... some sort of global illumination (e.g. - // radiosity) is of course the "right" solution, but maybe we can find - // something cheaper? - let shadow = 0.2 + 0.8 * shade_frac; - - let lambertian = l_m.dot(n).max(0.0); - let spec_angle = r_m.dot(v).max(0.0); - - let ambient = k_a * i_a; - let diffuse = k_d * lambertian * i_m_d; - let specular = k_s * spec_angle.powf(alpha) * i_m_s; - (ambient + shadow * (diffuse + specular)).map(|e| e.min(1.0)) + }) + .map(|sample| { + // TODO: Eliminate the redundancy between this and the block renderer. + let alt = sample.alt; + let basement = sample.basement; + let grass_depth = (1.5 + 2.0 * sample.chaos).min(alt - basement); + let wposz = if is_basement { basement } else { alt }; + if is_basement && wposz < alt - grass_depth { + Lerp::lerp( + sample.sub_surface_color, + sample.stone_col.map(|e| e as f32 / 255.0), + (alt - grass_depth - wposz as f32) * 0.15, + ) + .map(|e| e as f64) } else { - rgb + Lerp::lerp( + sample.sub_surface_color, + sample.surface_color, + ((wposz as f32 - (alt - grass_depth)) / grass_depth).powf(0.5), + ) + .map(|e| e as f64) } - .map(|e| (e * 255.0) as u8); - - let rgba = (rgb.r, rgb.g, rgb.b, 255); - write_pixel(Vec2::new(i, j), rgba); }); - MapDebug { - quads, - rivers, - lakes, - oceans, - } + let downhill_wpos = downhill + .map(|downhill_pos| downhill_pos) + .unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32)); + let alt = if is_basement { basement } else { alt }; + + let true_water_alt = (alt.max(water_alt) as f64 - focus.z) / gain as f64; + let true_alt = (alt as f64 - focus.z) / gain as f64; + let water_depth = (true_water_alt - true_alt).min(1.0).max(0.0); + let alt = true_alt.min(1.0).max(0.0); + + let water_color_factor = 2.0; + let g_water = 32.0 * water_color_factor; + let b_water = 64.0 * water_color_factor; + let default_rgb = Rgb::new( + if is_shaded || is_temperature { + 1.0 + } else { + 0.0 + }, + if is_shaded { 1.0 } else { alt }, + if is_shaded || is_humidity { 1.0 } else { 0.0 }, + ); + let column_rgb = column_rgb.unwrap_or(default_rgb); + let mut connections = [None; 8]; + let mut has_connections = false; + // TODO: Support non-river connections. + // TODO: Support multiple connections. + let river_width = river_kind.map(|river| match river { + RiverKind::River { cross_section } => cross_section.x, + RiverKind::Lake { .. } | RiverKind::Ocean => TerrainChunkSize::RECT_SIZE.x as f32, + }); + if let (Some(river_width), true) = (river_width, is_water) { + let downhill_pos = downhill_wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); + NEIGHBOR_DELTA + .iter() + .zip((&mut connections).iter_mut()) + .filter(|&(&offset, _)| downhill_pos - pos == Vec2::from(offset)) + .for_each(|(_, connection)| { + has_connections = true; + *connection = Some(Connection { + kind: ConnectionKind::River, + spline_derivative, + width: river_width, + }); + }); + }; + let rgb = match (river_kind, (is_water, true_alt >= true_sea_level)) { + (_, (false, _)) | (None, (_, true)) | (Some(RiverKind::River { .. }), _) => { + let (r, g, b) = ( + (column_rgb.r + * if is_temperature { + temperature as f64 + } else { + column_rgb.r + }) + .sqrt(), + column_rgb.g, + (column_rgb.b + * if is_humidity { + humidity as f64 + } else { + column_rgb.b + }) + .sqrt(), + ); + Rgb::new((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8) + }, + (None, _) | (Some(RiverKind::Lake { .. }), _) | (Some(RiverKind::Ocean), _) => Rgb::new( + 0, + ((g_water - water_depth * g_water) * 1.0) as u8, + ((b_water - water_depth * b_water) * 1.0) as u8, + ), + }; + // TODO: Make principled. + let rgb = if near_site { + Rgb::new(0x57, 0x39, 0x33) + } else if is_path { + Rgb::new(0x37, 0x29, 0x23) + } else { + rgb + }; + + MapSample { + rgb, + alt: if is_water { + true_alt.max(true_water_alt) + } else { + true_alt + }, + downhill_wpos, + connections: if has_connections { + Some(connections) + } else { + None + }, } } diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 95fa08ba54..69fa1d7334 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -14,12 +14,11 @@ pub use self::{ get_rivers, mrec_downhill, Alt, RiverData, RiverKind, }, location::Location, - map::{MapConfig, MapDebug, MapSample}, + map::{sample_pos, sample_wpos}, path::PathData, util::{ cdf_irwin_hall, downhill, get_horizon_map, get_oceans, local_cells, map_edge_factor, - neighbors, uniform_idx_as_vec2, uniform_noise, uphill, vec2_as_uniform_idx, InverseCdf, - ScaleBias, NEIGHBOR_DELTA, + uniform_noise, uphill, InverseCdf, ScaleBias, }, }; @@ -36,7 +35,10 @@ use common::{ assets, msg::server::WorldMapMsg, store::Id, - terrain::{BiomeKind, TerrainChunkSize}, + terrain::{ + map::MapConfig, uniform_idx_as_vec2, vec2_as_uniform_idx, BiomeKind, MapSizeLg, + TerrainChunkSize, + }, vol::RectVolSize, }; use noise::{ @@ -58,6 +60,18 @@ use std::{ use tracing::{debug, warn}; use vek::*; +/// Default base two logarithm of the world size, in chunks, per dimension. +/// +/// Currently, our default map dimensions are 2^10 × 2^10 chunks, +/// mostly for historical reasons. It is likely that we will increase this +/// default at some point. +const DEFAULT_WORLD_CHUNKS_LG: MapSizeLg = + if let Ok(map_size_lg) = MapSizeLg::new(Vec2 { x: 10, y: 10 }) { + map_size_lg + } else { + panic!("Default world chunk size does not satisfy required invariants."); + }; + // NOTE: I suspect this is too small (1024 * 16 * 1024 * 16 * 8 doesn't fit in // an i32), but we'll see what happens, I guess! We could always store sizes >> // 3. I think 32 or 64 is the absolute limit though, and would require @@ -67,9 +81,9 @@ use vek::*; // don't think we actually cast a chunk id to float, just coordinates... could // be wrong though! #[allow(clippy::identity_op)] // TODO: Pending review in #587 -pub const WORLD_SIZE: Vec2 = Vec2 { - x: 1024 * 1, - y: 1024 * 1, +const WORLD_SIZE: Vec2 = Vec2 { + x: 1 << DEFAULT_WORLD_CHUNKS_LG.vec().x, + y: 1 << DEFAULT_WORLD_CHUNKS_LG.vec().y, }; /// A structure that holds cached noise values and cumulative distribution @@ -296,6 +310,8 @@ impl WorldFile { pub struct WorldSim { pub seed: u32, + /// Base 2 logarithm of the map size. + map_size_lg: MapSizeLg, /// Maximum height above sea level of any chunk in the map (not including /// post-erosion warping, cliffs, and other things like that). pub max_height: f32, @@ -449,16 +465,17 @@ impl WorldSim { // Assumes μ = 0, σ = 1 let logistic_cdf = |x: f64| (x / logistic_2_base).tanh() * 0.5 + 0.5; - let min_epsilon = - 1.0 / (WORLD_SIZE.x as f64 * WORLD_SIZE.y as f64).max(f64::EPSILON as f64 * 0.5); - let max_epsilon = (1.0 - 1.0 / (WORLD_SIZE.x as f64 * WORLD_SIZE.y as f64)) - .min(1.0 - f64::EPSILON as f64 * 0.5); + let map_size_lg = DEFAULT_WORLD_CHUNKS_LG; + let map_size_chunks_len_f64 = map_size_lg.chunks().map(f64::from).product(); + let min_epsilon = 1.0 / map_size_chunks_len_f64.max(f64::EPSILON as f64 * 0.5); + let max_epsilon = + (1.0 - 1.0 / map_size_chunks_len_f64).min(1.0 - f64::EPSILON as f64 * 0.5); // No NaNs in these uniform vectors, since the original noise value always // returns Some. let ((alt_base, _), (chaos, _)) = rayon::join( || { - uniform_noise(|_, wposf| { + uniform_noise(map_size_lg, |_, wposf| { // "Base" of the chunk, to be multiplied by CONFIG.mountain_scale (multiplied // value is from -0.35 * (CONFIG.mountain_scale * 1.05) to // 0.35 * (CONFIG.mountain_scale * 0.95), but value here is from -0.3675 to @@ -475,7 +492,7 @@ impl WorldSim { }) }, || { - uniform_noise(|_, wposf| { + uniform_noise(map_size_lg, |_, wposf| { // From 0 to 1.6, but the distribution before the max is from -1 and 1.6, so // there is a 50% chance that hill will end up at 0.3 or // lower, and probably a very high change it will be exactly @@ -548,7 +565,7 @@ impl WorldSim { // // No NaNs in these uniform vectors, since the original noise value always // returns Some. - let (alt_old, _) = uniform_noise(|posi, wposf| { + let (alt_old, _) = uniform_noise(map_size_lg, |posi, wposf| { // This is the extension upwards from the base added to some extra noise from -1 // to 1. // @@ -612,11 +629,11 @@ impl WorldSim { // = [-0.946, 1.067] Some( ((alt_base[posi].1 + alt_main.mul((chaos[posi].1 as f64).powf(1.2))) - .mul(map_edge_factor(posi) as f64) + .mul(map_edge_factor(map_size_lg, posi) as f64) .add( (CONFIG.sea_level as f64) .div(CONFIG.mountain_scale as f64) - .mul(map_edge_factor(posi) as f64), + .mul(map_edge_factor(map_size_lg, posi) as f64), ) .sub((CONFIG.sea_level as f64).div(CONFIG.mountain_scale as f64))) as f32, @@ -624,12 +641,12 @@ impl WorldSim { }); // Calculate oceans. - let is_ocean = get_oceans(|posi: usize| alt_old[posi].1); + let is_ocean = get_oceans(map_size_lg, |posi: usize| alt_old[posi].1); // NOTE: Uncomment if you want oceans to exclusively be on the border of the // map. - /* let is_ocean = (0..WORLD_SIZE.x * WORLD_SIZE.y) + /* let is_ocean = (0..map_size_lg.chunks()) .into_par_iter() - .map(|i| map_edge_factor(i) == 0.0) + .map(|i| map_edge_factor(map_size_lg, i) == 0.0) .collect::>(); */ let is_ocean_fn = |posi: usize| is_ocean[posi]; @@ -650,14 +667,14 @@ impl WorldSim { // Recalculate altitudes without oceans. // NaNs in these uniform vectors wherever is_ocean_fn returns true. - let (alt_old_no_ocean, _) = uniform_noise(|posi, _| { + let (alt_old_no_ocean, _) = uniform_noise(map_size_lg, |posi, _| { if is_ocean_fn(posi) { None } else { Some(old_height(posi)) } }); - let (uplift_uniform, _) = uniform_noise(|posi, _wposf| { + let (uplift_uniform, _) = uniform_noise(map_size_lg, |posi, _wposf| { if is_ocean_fn(posi) { None } else { @@ -717,7 +734,7 @@ impl WorldSim { } }; let g_func = |posi| { - if map_edge_factor(posi) == 0.0 { + if map_edge_factor(map_size_lg, posi) == 0.0 { return 0.0; } // G = d* v_s / p_0, where @@ -743,8 +760,9 @@ impl WorldSim { let epsilon_0_i = 2.078e-3 / 4.0; return epsilon_0_i * epsilon_0_scale_i; } - let wposf = (uniform_idx_as_vec2(posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) - .map(|e| e as f64); + let wposf = (uniform_idx_as_vec2(map_size_lg, posi) + * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) + .map(|e| e as f64); let turb_wposf = wposf .mul(5_000.0 / continent_scale) .div(TerrainChunkSize::RECT_SIZE.map(|e| e as f64)) @@ -797,8 +815,9 @@ impl WorldSim { // marine: α = 3.7e-2 return 3.7e-2 * alpha_scale_i; } - let wposf = (uniform_idx_as_vec2(posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) - .map(|e| e as f64); + let wposf = (uniform_idx_as_vec2(map_size_lg, posi) + * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) + .map(|e| e as f64); let turb_wposf = wposf .mul(5_000.0 / continent_scale) .div(TerrainChunkSize::RECT_SIZE.map(|e| e as f64)) @@ -963,10 +982,11 @@ impl WorldSim { // Perform some erosion. - let (alt, basement) = if let Some(map) = parsed_world_file { - (map.alt, map.basement) + let (alt, basement, map_size_lg) = if let Some(map) = parsed_world_file { + (map.alt, map.basement, DEFAULT_WORLD_CHUNKS_LG) } else { let (alt, basement) = do_erosion( + map_size_lg, max_erosion_per_delta_t as f32, n_steps, &river_seed, @@ -992,7 +1012,8 @@ impl WorldSim { ); // Quick "small scale" erosion cycle in order to lower extreme angles. - do_erosion( + let (alt, basement) = do_erosion( + map_size_lg, 1.0f32, n_small_steps, &river_seed, @@ -1011,7 +1032,9 @@ impl WorldSim { height_scale, k_d_scale(n_approx), k_da_scale, - ) + ); + + (alt, basement, map_size_lg) }; // Save map, if necessary. @@ -1060,6 +1083,7 @@ impl WorldSim { (alt, basement) } else { do_erosion( + map_size_lg, 1.0f32, n_post_load_steps, &river_seed, @@ -1081,28 +1105,31 @@ impl WorldSim { ) }; - let is_ocean = get_oceans(|posi| alt[posi]); + let is_ocean = get_oceans(map_size_lg, |posi| alt[posi]); let is_ocean_fn = |posi: usize| is_ocean[posi]; - let mut dh = downhill(|posi| alt[posi], is_ocean_fn); - let (boundary_len, indirection, water_alt_pos, maxh) = get_lakes(|posi| alt[posi], &mut dh); + let mut dh = downhill(map_size_lg, |posi| alt[posi], is_ocean_fn); + let (boundary_len, indirection, water_alt_pos, maxh) = + get_lakes(map_size_lg, |posi| alt[posi], &mut dh); debug!(?maxh, "Max height"); let (mrec, mstack, mwrec) = { - let mut wh = vec![0.0; WORLD_SIZE.x * WORLD_SIZE.y]; + let mut wh = vec![0.0; map_size_lg.chunks_len()]; get_multi_rec( + map_size_lg, |posi| alt[posi], &dh, &water_alt_pos, &mut wh, - WORLD_SIZE.x, - WORLD_SIZE.y, + usize::from(map_size_lg.chunks().x), + usize::from(map_size_lg.chunks().y), TerrainChunkSize::RECT_SIZE.x as Compute, TerrainChunkSize::RECT_SIZE.y as Compute, maxh, ) }; - let flux_old = get_multi_drainage(&mstack, &mrec, &*mwrec, boundary_len); - // let flux_rivers = get_drainage(&water_alt_pos, &dh, boundary_len); - // TODO: Make rivers work with multi-direction flux as well. + let flux_old = get_multi_drainage(map_size_lg, &mstack, &mrec, &*mwrec, boundary_len); + // let flux_rivers = get_drainage(map_size_lg, &water_alt_pos, &dh, + // boundary_len); TODO: Make rivers work with multi-direction flux as + // well. let flux_rivers = flux_old.clone(); let water_height_initial = |chunk_idx| { @@ -1153,13 +1180,20 @@ impl WorldSim { // may comment out this line and replace it with the commented-out code // below; however, there are no guarantees that this // will work correctly. - let water_alt = fill_sinks(water_height_initial, is_ocean_fn); - /* let water_alt = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let water_alt = fill_sinks(map_size_lg, water_height_initial, is_ocean_fn); + /* let water_alt = (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| water_height_initial(posi)) .collect::>(); */ - let rivers = get_rivers(&water_alt_pos, &water_alt, &dh, &indirection, &flux_rivers); + let rivers = get_rivers( + map_size_lg, + &water_alt_pos, + &water_alt, + &dh, + &indirection, + &flux_rivers, + ); let water_alt = indirection .par_iter() @@ -1198,11 +1232,15 @@ impl WorldSim { // Check whether any tiles around this tile are not water (since Lerp will // ensure that they are included). let pure_water = |posi: usize| { - let pos = uniform_idx_as_vec2(posi); + let pos = uniform_idx_as_vec2(map_size_lg, posi); for x in pos.x - 1..(pos.x + 1) + 1 { for y in pos.y - 1..(pos.y + 1) + 1 { - if x >= 0 && y >= 0 && x < WORLD_SIZE.x as i32 && y < WORLD_SIZE.y as i32 { - let posi = vec2_as_uniform_idx(Vec2::new(x, y)); + if x >= 0 + && y >= 0 + && x < map_size_lg.chunks().x as i32 + && y < map_size_lg.chunks().y as i32 + { + let posi = vec2_as_uniform_idx(map_size_lg, Vec2::new(x, y)); if !is_underwater(posi) { return false; } @@ -1217,7 +1255,7 @@ impl WorldSim { || { rayon::join( || { - uniform_noise(|posi, _| { + uniform_noise(map_size_lg, |posi, _| { if pure_water(posi) { None } else { @@ -1228,7 +1266,7 @@ impl WorldSim { }) }, || { - uniform_noise(|posi, _| { + uniform_noise(map_size_lg, |posi, _| { if pure_water(posi) { None } else { @@ -1241,7 +1279,7 @@ impl WorldSim { || { rayon::join( || { - uniform_noise(|posi, wposf| { + uniform_noise(map_size_lg, |posi, wposf| { if pure_water(posi) { None } else { @@ -1251,7 +1289,7 @@ impl WorldSim { }) }, || { - uniform_noise(|posi, wposf| { + uniform_noise(map_size_lg, |posi, wposf| { // Check whether any tiles around this tile are water. if pure_water(posi) { None @@ -1283,13 +1321,14 @@ impl WorldSim { rivers, }; - let chunks = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let chunks = (0..map_size_lg.chunks_len()) .into_par_iter() - .map(|i| SimChunk::generate(i, &gen_ctx, &gen_cdf)) + .map(|i| SimChunk::generate(map_size_lg, i, &gen_ctx, &gen_cdf)) .collect::>(); let mut this = Self { seed, + map_size_lg, max_height: maxh as f32, chunks, locations: Vec::new(), @@ -1304,13 +1343,18 @@ impl WorldSim { this } - pub fn get_size(&self) -> Vec2 { WORLD_SIZE.map(|e| e as u32) } + #[inline(always)] + pub const fn map_size_lg(&self) -> MapSizeLg { self.map_size_lg } + + pub fn get_size(&self) -> Vec2 { self.map_size_lg().chunks().map(u32::from) } /// Draw a map of the world based on chunk information. Returns a buffer of /// u32s. pub fn get_map(&self) -> WorldMapMsg { - let mut map_config = MapConfig::default(); - map_config.lgain = 1.0; + let mut map_config = MapConfig::orthographic( + DEFAULT_WORLD_CHUNKS_LG, + core::ops::RangeInclusive::new(CONFIG.sea_level, CONFIG.sea_level + self.max_height), + ); // Build a horizon map. let scale_angle = |angle: Alt| { (/* 0.0.max( */angle /* ) */ @@ -1325,14 +1369,14 @@ impl WorldSim { let samples_data = { let column_sample = ColumnGen::new(self); - (0..WORLD_SIZE.product()) + (0..self.map_size_lg().chunks_len()) .into_par_iter() .map_init( || Box::new(BlockGen::new(ColumnGen::new(self))), |block_gen, posi| { - let wpos = uniform_idx_as_vec2(posi); + let wpos = uniform_idx_as_vec2(self.map_size_lg(), posi); let mut sample = column_sample.get( - uniform_idx_as_vec2(posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + uniform_idx_as_vec2(self.map_size_lg(), posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), )?; let alt = sample.alt; /* let z_cache = block_gen.get_z_cache(wpos); @@ -1353,7 +1397,7 @@ impl WorldSim { ) /* .map(|posi| { let mut sample = column_sample.get( - uniform_idx_as_vec2(posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + uniform_idx_as_vec2(self.map_size_lg(), posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), ); }) */ .collect::>() @@ -1361,55 +1405,53 @@ impl WorldSim { }; let horizons = get_horizon_map( - map_config.lgain, + self.map_size_lg(), Aabr { min: Vec2::zero(), - max: WORLD_SIZE.map(|e| e as i32), + max: self.map_size_lg().chunks().map(|e| e as i32), }, - CONFIG.sea_level as Alt, - (CONFIG.sea_level + self.max_height) as Alt, + CONFIG.sea_level, + CONFIG.sea_level + self.max_height, |posi| { /* let chunk = &self.chunks[posi]; chunk.alt.max(chunk.water_alt) as Alt */ let sample = samples_data[posi].as_ref(); sample .map(|s| s.alt.max(s.water_level)) - .unwrap_or(CONFIG.sea_level) as Alt + .unwrap_or(CONFIG.sea_level) }, - |a| scale_angle(a), - |h| scale_height(h), + |a| scale_angle(a.into()), + |h| scale_height(h.into()), ) .unwrap(); - let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y]; - let mut alts = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y]; + let mut v = vec![0u32; self.map_size_lg().chunks_len()]; + let mut alts = vec![0u32; self.map_size_lg().chunks_len()]; // TODO: Parallelize again. - let config = MapConfig { - gain: self.max_height, - samples: Some(&samples_data), - is_shaded: false, - ..map_config - }; + map_config.is_shaded = false; - config.generate( - |pos| config.sample_pos(self, pos), - |pos| config.sample_wpos(self, pos), + map_config.generate( + |pos| sample_pos(&map_config, self, Some(&samples_data), pos), + |pos| sample_wpos(&map_config, self, pos), |pos, (r, g, b, _a)| { // We currently ignore alpha and replace it with the height at pos, scaled to // u8. - let alt = config.sample_wpos( + let alt = sample_wpos( + &map_config, self, pos.map(|e| e as i32) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), ); let a = 0; //(alt.min(1.0).max(0.0) * 255.0) as u8; - let posi = pos.y * WORLD_SIZE.x + pos.x; + // NOTE: Safe by invariants on map_size_lg. + let posi = (pos.y << self.map_size_lg().vec().x) | pos.x; v[posi] = u32::from_le_bytes([r, g, b, a]); alts[posi] = (((alt.min(1.0).max(0.0) * 8191.0) as u32) & 0x1FFF) << 3; }, ); WorldMapMsg { - dimensions: WORLD_SIZE.map(|e| e as u16), + dimensions_lg: self.map_size_lg().vec(), + sea_level: CONFIG.sea_level, max_height: self.max_height, rgba: v, alt: alts, @@ -1422,7 +1464,7 @@ impl WorldSim { let mut rng = self.rng.clone(); let cell_size = 16; - let grid_size = WORLD_SIZE / cell_size; + let grid_size = self.map_size_lg().chunks().map(usize::from) / cell_size; let loc_count = 100; let mut loc_grid = vec![None; grid_size.product()]; @@ -1490,7 +1532,7 @@ impl WorldSim { .par_iter_mut() .enumerate() .for_each(|(ij, chunk)| { - let chunk_pos = uniform_idx_as_vec2(ij); + let chunk_pos = uniform_idx_as_vec2(self.map_size_lg(), ij); let i = chunk_pos.x as usize; let j = chunk_pos.y as usize; let block_pos = Vec2::new( @@ -1530,10 +1572,10 @@ impl WorldSim { // Create waypoints const WAYPOINT_EVERY: usize = 16; let this = &self; - let waypoints = (0..WORLD_SIZE.x) + let waypoints = (0..this.map_size_lg().chunks().x) .step_by(WAYPOINT_EVERY) .map(|i| { - (0..WORLD_SIZE.y) + (0..this.map_size_lg().chunks().y) .step_by(WAYPOINT_EVERY) .map(move |j| (i, j)) }) @@ -1576,10 +1618,10 @@ impl WorldSim { pub fn get(&self, chunk_pos: Vec2) -> Option<&SimChunk> { if chunk_pos - .map2(WORLD_SIZE, |e, sz| e >= 0 && e < sz as i32) + .map2(self.map_size_lg().chunks(), |e, sz| e >= 0 && e < sz as i32) .reduce_and() { - Some(&self.chunks[vec2_as_uniform_idx(chunk_pos)]) + Some(&self.chunks[vec2_as_uniform_idx(self.map_size_lg(), chunk_pos)]) } else { None } @@ -1607,11 +1649,12 @@ impl WorldSim { } pub fn get_mut(&mut self, chunk_pos: Vec2) -> Option<&mut SimChunk> { + let map_size_lg = self.map_size_lg(); if chunk_pos - .map2(WORLD_SIZE, |e, sz| e >= 0 && e < sz as i32) + .map2(map_size_lg.chunks(), |e, sz| e >= 0 && e < sz as i32) .reduce_and() { - Some(&mut self.chunks[vec2_as_uniform_idx(chunk_pos)]) + Some(&mut self.chunks[vec2_as_uniform_idx(map_size_lg, chunk_pos)]) } else { None } @@ -1619,16 +1662,18 @@ impl WorldSim { pub fn get_base_z(&self, chunk_pos: Vec2) -> Option { if !chunk_pos - .map2(WORLD_SIZE, |e, sz| e > 0 && e < sz as i32 - 2) + .map2(self.map_size_lg().chunks(), |e, sz| { + e > 0 && e < sz as i32 - 2 + }) .reduce_and() { return None; } - let chunk_idx = vec2_as_uniform_idx(chunk_pos); - local_cells(chunk_idx) + let chunk_idx = vec2_as_uniform_idx(self.map_size_lg(), chunk_pos); + local_cells(self.map_size_lg(), chunk_idx) .flat_map(|neighbor_idx| { - let neighbor_pos = uniform_idx_as_vec2(neighbor_idx); + let neighbor_pos = uniform_idx_as_vec2(self.map_size_lg(), neighbor_idx); let neighbor_chunk = self.get(neighbor_pos); let river_kind = neighbor_chunk.and_then(|c| c.river.river_kind); let has_water = river_kind.is_some() && river_kind != Some(RiverKind::Ocean); @@ -1904,11 +1949,10 @@ pub struct RegionInfo { impl SimChunk { #[allow(clippy::if_same_then_else)] // TODO: Pending review in #587 #[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587 - fn generate(posi: usize, gen_ctx: &GenCtx, gen_cdf: &GenCdf) -> Self { - let pos = uniform_idx_as_vec2(posi); + fn generate(map_size_lg: MapSizeLg, posi: usize, gen_ctx: &GenCtx, gen_cdf: &GenCdf) -> Self { + let pos = uniform_idx_as_vec2(map_size_lg, posi); let wposf = (pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)).map(|e| e as f64); - let _map_edge_factor = map_edge_factor(posi); let (_, chaos) = gen_cdf.chaos[posi]; let alt_pre = gen_cdf.alt[posi] as f32; let basement_pre = gen_cdf.basement[posi] as f32; @@ -1929,7 +1973,7 @@ impl SimChunk { // can always add a small x component). // // Not clear that we want this yet, let's see. - let latitude_uniform = (pos.y as f32 / WORLD_SIZE.y as f32).sub(0.5).mul(2.0); + let latitude_uniform = (pos.y as f32 / f32::from(self.map_size_lg().chunks().y)).sub(0.5).mul(2.0); // Even less granular--if this matters we can make the sign affect the quantiy slightly. let abs_lat_uniform = latitude_uniform.abs(); */ @@ -1962,7 +2006,7 @@ impl SimChunk { panic!("Uh... shouldn't this never, ever happen?"); } else { Some( - uniform_idx_as_vec2(downhill_pre as usize) + uniform_idx_as_vec2(map_size_lg, downhill_pre as usize) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), ) }; diff --git a/world/src/sim/util.rs b/world/src/sim/util.rs index 1e2aca9793..3fda529d13 100644 --- a/world/src/sim/util.rs +++ b/world/src/sim/util.rs @@ -1,6 +1,8 @@ -use super::WORLD_SIZE; use bitvec::prelude::{bitbox, BitBox}; -use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use common::{ + terrain::{neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, MapSizeLg, TerrainChunkSize}, + vol::RectVolSize, +}; use noise::{MultiFractal, NoiseFn, Perlin, Point2, Point3, Point4, Seedable}; use num::Float; use rayon::prelude::*; @@ -8,14 +10,15 @@ use std::{f32, f64, ops::Mul, u32}; use vek::*; /// 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). -pub fn map_edge_factor(posi: usize) -> f32 { - uniform_idx_as_vec2(posi) - .map2(WORLD_SIZE.map(|e| e as i32), |e, sz| { +/// the world. This value is maximal at map_size_lg.chunks() / 2 and +/// minimized at the +/// extremes (0 or map_size_lg.chunks() 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). +pub fn map_edge_factor(map_size_lg: MapSizeLg, posi: usize) -> f32 { + uniform_idx_as_vec2(map_size_lg, posi) + .map2(map_size_lg.chunks().map(i32::from), |e, sz| { (sz / 2 - (e - sz / 2).abs()) as f32 / (16.0 / 1024.0 * sz as f32) }) .reduce_partial_min() @@ -118,8 +121,6 @@ pub fn cdf_irwin_hall(weights: &[f32; N], samples: [f32; N]) -> /// 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, F)]>; /// NOTE: First component is estimated horizon angles at each chunk; second @@ -127,19 +128,6 @@ pub type InverseCdf = Box<[(f32, F)]>; /// for making shadows volumetric). pub type HorizonMap = (Vec, Vec); -/// 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 { - Vec2::new((idx % WORLD_SIZE.x) as i32, (idx / WORLD_SIZE.x) as i32) -} - -/// Computes the index of a Vec2 of a SimChunk from a position, where the index -/// is generated by uniform_noise. NOTE: Both components of idx should be -/// in-bounds! -pub fn vec2_as_uniform_idx(idx: Vec2) -> usize { - (idx.y as usize * WORLD_SIZE.x + idx.x as usize) as usize -} - /// 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 @@ -178,15 +166,17 @@ pub fn vec2_as_uniform_idx(idx: Vec2) -> usize { /// value actually uses the same one we were using here easier). Also returns /// the "inverted index" pointing from a position to a noise. pub fn uniform_noise( + map_size_lg: MapSizeLg, f: impl Fn(usize, Vec2) -> Option + Sync, ) -> (InverseCdf, Box<[(usize, F)]>) { - let mut noise = (0..WORLD_SIZE.x * WORLD_SIZE.y) + let mut noise = (0..map_size_lg.chunks_len()) .into_par_iter() .filter_map(|i| { f( i, - (uniform_idx_as_vec2(i) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) - .map(|e| e as f64), + (uniform_idx_as_vec2(map_size_lg, i) + * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) + .map(|e| e as f64), ) .map(|res| (i, res)) }) @@ -202,7 +192,7 @@ pub fn uniform_noise( // position of the noise in the sorted vector (divided by the vector length). // This guarantees a uniform distribution among the samples (excluding those // that returned None, which will remain at zero). - let mut uniform_noise = vec![(0.0, F::nan()); WORLD_SIZE.x * WORLD_SIZE.y].into_boxed_slice(); + let mut uniform_noise = vec![(0.0, F::nan()); map_size_lg.chunks_len()].into_boxed_slice(); // NOTE: Consider using try_into here and elsewhere in this function, since // i32::MAX technically doesn't fit in an f32 (even if we should never reach // that limit). @@ -223,8 +213,8 @@ pub fn uniform_noise( /// its top-right/down-right/down neighbors, the twelve chunks surrounding this /// box (its "perimeter") are also inspected. #[allow(clippy::useless_conversion)] // TODO: Pending review in #587 -pub fn local_cells(posi: usize) -> impl Clone + Iterator { - let pos = uniform_idx_as_vec2(posi); +pub fn local_cells(map_size_lg: MapSizeLg, posi: usize) -> impl Clone + Iterator { + let pos = uniform_idx_as_vec2(map_size_lg, posi); // NOTE: want to keep this such that the chunk index is in ascending order! let grid_size = 3i32; let grid_bounds = 2 * grid_size + 1; @@ -236,51 +226,35 @@ pub fn local_cells(posi: usize) -> impl Clone + Iterator { pos.y + (index / grid_bounds) - grid_size, ) }) - .filter(|pos| { - pos.x >= 0 && pos.y >= 0 && pos.x < WORLD_SIZE.x as i32 && pos.y < WORLD_SIZE.y as i32 + .filter(move |pos| { + pos.x >= 0 + && pos.y >= 0 + && pos.x < map_size_lg.chunks().x as i32 + && pos.y < map_size_lg.chunks().y as i32 }) - .map(vec2_as_uniform_idx) -} - -// NOTE: want to keep this such that the chunk index is in ascending order! -pub const NEIGHBOR_DELTA: [(i32, i32); 8] = [ - (-1, -1), - (0, -1), - (1, -1), - (-1, 0), - (1, 0), - (-1, 1), - (0, 1), - (1, 1), -]; - -/// Iterate through all cells adjacent to a chunk. -pub fn neighbors(posi: usize) -> impl Clone + Iterator { - let pos = uniform_idx_as_vec2(posi); - NEIGHBOR_DELTA - .iter() - .map(move |&(x, y)| Vec2::new(pos.x + x, pos.y + y)) - .filter(|pos| { - pos.x >= 0 && pos.y >= 0 && pos.x < WORLD_SIZE.x as i32 && pos.y < WORLD_SIZE.y as i32 - }) - .map(vec2_as_uniform_idx) + .map(move |e| vec2_as_uniform_idx(map_size_lg, e)) } // Note that we should already have okay cache locality since we have a grid. -pub fn uphill<'a>(dh: &'a [isize], posi: usize) -> impl Clone + Iterator + 'a { - neighbors(posi).filter(move |&posj| dh[posj] == posi as isize) +pub fn uphill<'a>( + map_size_lg: MapSizeLg, + dh: &'a [isize], + posi: usize, +) -> impl Clone + Iterator + 'a { + neighbors(map_size_lg, posi).filter(move |&posj| dh[posj] == posi as isize) } /// Compute the neighbor "most downhill" from all chunks. /// /// TODO: See if allocating in advance is worthwhile. pub fn downhill( + map_size_lg: MapSizeLg, h: impl Fn(usize) -> F + Sync, is_ocean: impl Fn(usize) -> bool + Sync, ) -> Box<[isize]> { // Constructs not only the list of downhill nodes, but also computes an ordering // (visiting nodes in order from roots to leaves). - (0..WORLD_SIZE.x * WORLD_SIZE.y) + (0..map_size_lg.chunks_len()) .into_par_iter() .map(|posi| { let nh = h(posi); @@ -289,7 +263,7 @@ pub fn downhill( } else { let mut best = -1; let mut besth = nh; - for nposi in neighbors(posi) { + for nposi in neighbors(map_size_lg, posi) { let nbh = h(nposi); if nbh < besth { besth = nbh; @@ -354,37 +328,37 @@ fn get_interpolated_bilinear(&self, pos: Vec2, mut f: F) -> Option /// - posi is at the side of the world (map_edge_factor(posi) == 0.0) /// - posi has a neighboring ocean tile, and has a height below sea level /// (oldh(posi) <= 0.0). -pub fn get_oceans(oldh: impl Fn(usize) -> F + Sync) -> BitBox { +pub fn get_oceans(map_size_lg: MapSizeLg, oldh: impl Fn(usize) -> F + Sync) -> BitBox { // We can mark tiles as ocean candidates by scanning row by row, since the top // edge is ocean, the sides are connected to it, and any subsequent ocean // tiles must be connected to it. - let mut is_ocean = bitbox![0; WORLD_SIZE.x * WORLD_SIZE.y]; + let mut is_ocean = bitbox![0; map_size_lg.chunks_len()]; let mut stack = Vec::new(); let mut do_push = |pos| { - let posi = vec2_as_uniform_idx(pos); + let posi = vec2_as_uniform_idx(map_size_lg, pos); if oldh(posi) <= F::zero() { stack.push(posi); } }; - for x in 0..WORLD_SIZE.x as i32 { + for x in 0..map_size_lg.chunks().x as i32 { do_push(Vec2::new(x, 0)); - do_push(Vec2::new(x, WORLD_SIZE.y as i32 - 1)); + do_push(Vec2::new(x, map_size_lg.chunks().y as i32 - 1)); } - for y in 1..WORLD_SIZE.y as i32 - 1 { + for y in 1..map_size_lg.chunks().y as i32 - 1 { do_push(Vec2::new(0, y)); - do_push(Vec2::new(WORLD_SIZE.x as i32 - 1, y)); + do_push(Vec2::new(map_size_lg.chunks().x as i32 - 1, y)); } while let Some(chunk_idx) = stack.pop() { - // println!("Ocean chunk {:?}: {:?}", uniform_idx_as_vec2(chunk_idx), - // oldh(chunk_idx)); + // println!("Ocean chunk {:?}: {:?}", uniform_idx_as_vec2(map_size_lg, + // chunk_idx), oldh(chunk_idx)); let mut is_ocean = is_ocean.get_mut(chunk_idx).unwrap(); if *is_ocean { continue; } *is_ocean = true; - stack.extend(neighbors(chunk_idx).filter(|&neighbor_idx| { - // println!("Ocean neighbor: {:?}: {:?}", uniform_idx_as_vec2(neighbor_idx), - // oldh(neighbor_idx)); + stack.extend(neighbors(map_size_lg, chunk_idx).filter(|&neighbor_idx| { + // println!("Ocean neighbor: {:?}: {:?}", uniform_idx_as_vec2(map_size_lg, + // neighbor_idx), oldh(neighbor_idx)); oldh(neighbor_idx) <= F::zero() })); } @@ -393,7 +367,7 @@ pub fn get_oceans(oldh: impl Fn(usize) -> F + Sync) -> BitBox { /// Finds the horizon map for sunlight for the given chunks. pub fn get_horizon_map( - lgain: F, + map_size_lg: MapSizeLg, bounds: Aabr, minh: F, maxh: F, @@ -416,7 +390,7 @@ pub fn get_horizon_map( }; // let epsilon = F::epsilon() * if let x = F::from(map_size.x) { x } else { // return Err(()) }; - let march = |dx: isize, maxdx: fn(isize) -> isize| { + let march = |dx: isize, maxdx: fn(isize, map_size_lg: MapSizeLg) -> isize| { let mut angles = Vec::with_capacity(map_len); let mut heights = Vec::with_capacity(map_len); (0..map_len) @@ -425,14 +399,14 @@ pub fn get_horizon_map( let wposi = bounds.min + Vec2::new((posi % map_size.x) as i32, (posi / map_size.x) as i32); if wposi.reduce_partial_min() < 0 - || wposi.x as usize >= WORLD_SIZE.x - || wposi.y as usize >= WORLD_SIZE.y + || wposi.x as usize >= usize::from(map_size_lg.chunks().x) + || wposi.y as usize >= usize::from(map_size_lg.chunks().y) { return (to_angle(F::zero()), to_height(F::zero())); } - let posi = vec2_as_uniform_idx(wposi); + let posi = vec2_as_uniform_idx(map_size_lg, wposi); // March in the given direction. - let maxdx = maxdx(wposi.x as isize); + let maxdx = maxdx(wposi.x as isize, map_size_lg); let mut slope = F::zero(); let h0 = h(posi); let h = if h0 < minh { @@ -458,14 +432,16 @@ pub fn get_horizon_map( } h0 - minh + max_height }; - let a = slope * lgain; + let a = slope; (to_angle(a), to_height(h)) }) .unzip_into_vecs(&mut angles, &mut heights); (angles, heights) }; - let west = march(-1, |x| x); - let east = march(1, |x| (WORLD_SIZE.x - x as usize) as isize); + let west = march(-1, |x, _| x); + let east = march(1, |x, map_size_lg| { + (usize::from(map_size_lg.chunks().x) - x as usize) as isize + }); Ok([west, east]) }