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).
This commit is contained in:
Joshua Yanovski 2020-07-28 10:55:48 +02:00
parent 30b1d2c642
commit 13b6d4d534
19 changed files with 1593 additions and 1149 deletions

3
Cargo.lock generated
View File

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

View File

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

View File

@ -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 * <Alt as FloatConst>::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 * <f32 as FloatConst>::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<i32>| {
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::<Vec<_>>();
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((

View File

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

View File

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

View File

@ -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<u16>,
/// Log base 2 of world map dimensions (width × height) in chunks.
///
/// NOTE: Invariant: chunk count fits in a u16.
pub dimensions_lg: Vec2<u32>,
/// 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

719
common/src/terrain/map.rs Normal file
View File

@ -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<u32> = 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<u32>);
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<u32>) -> Result<Self, ()> {
// 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<u32> { self.0 }
#[inline(always)]
/// Get the size of this map in chunks.
pub const fn chunks(self) -> Vec2<u16> { 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<MapSizeLg> for Vec2<u32> {
#[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<usize>,
/// 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<f64>,
/// 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<f64>,
/// If Some, uses the provided horizon map.
///
/// Defaults to None.
pub horizons: Option<&'a [(Vec<f32>, Vec<f32>); 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<f32>,
/// 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<u8>,
/// 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<i32>,
/// 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<Connection>; 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<f32>) -> 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<i32>) -> MapSample,
sample_wpos: impl Fn(Vec2<i32>) -> f32,
mut write_pixel: impl FnMut(Vec2<usize>, (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,
}
}
}

View File

@ -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<u32> = Vec2 { x: 32, y: 32 };
const RECT_SIZE: Vec2<u32> = 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<Block, TerrainChunkSize, TerrainChunkMeta>;
pub type TerrainGrid = VolGrid2d<TerrainChunk>;
// 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<i32> {
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<i32>) -> 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<Item = usize> {
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<f64>,
spline_derivative: Vec2<f32>,
downhill_pos: Vec2<f64>,
) -> Vec3<Vec2<f64>> {
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<Vec2<f64>>,
point: Vec2<f64>,
) -> Option<(f64, Vec2<f64>, 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
}

View File

@ -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<i32> = WORLD_SIZE.map(|e| e as i32) / 2;
let center_chunk: Vec2<i32> = 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

View File

@ -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<usize> = 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<i32>,

View File

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

View File

@ -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::<Vec<_>>()
.into_boxed_slice()
};
let refresh_map_samples = |config: &MapConfig| {
(0..WORLD_SIZE.product())
let refresh_map_samples = |config: &MapConfig, samples: Option<&[Option<ColumnSample>]>| {
(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::<Vec<_>>()
.into_boxed_slice()
};
let get_map_sample = |map_samples: &[MapSample], pos: Vec2<i32>| {
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);

View File

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

View File

@ -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<f64>,
spline_derivative: Vec2<f32>,
downhill_pos: Vec2<f64>,
) -> Vec3<Vec2<f64>> {
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<Vec2<f64>>,
point: Vec2<f64>,
) -> Option<(f64, Vec2<f64>, 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<i32>;
type Sample = Option<ColumnSample<'a>>;
@ -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 {

View File

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

View File

@ -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<F: fmt::Debug + Float + Into<f64>, G: Float + Into<f64>>(
map_size_lg: MapSizeLg,
newh: &[u32],
water_alt: &[F],
downhill: &[isize],
@ -236,7 +246,7 @@ pub fn get_rivers<F: fmt::Debug + Float + Into<f64>, G: Float + Into<f64>>(
// 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<F: fmt::Debug + Float + Into<f64>, G: Float + Into<f64>>(
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<F: fmt::Debug + Float + Into<f64>, G: Float + Into<f64>>(
// 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<F: fmt::Debug + Float + Into<f64>, G: Float + Into<f64>>(
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<F: fmt::Debug + Float + Into<f64>, G: Float + Into<f64>>(
/// 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<Point3<f64>> + 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<F: Float + Send + Sync>(
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<F: Float + Send + Sync>(
// 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<F: Float + Send + Sync>(
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<F: Float + Send + Sync>(
/// indirection vector.
#[allow(clippy::filter_next)] // TODO: Pending review in #587
pub fn get_lakes<F: Float>(
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<F: Float>(
// 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<F: Float>(
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<F: Float>(
// 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<F: Float>(
"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<F: Float>(
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<F: Float>(
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<F: Float>(
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<F: Float>(
{
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<F: Float>(
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<F: Float>(
/// 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<Item = (usize, usize)> + '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<F: fmt::Debug + Float + Sync + Into<Compute>>(
map_size_lg: MapSizeLg,
h: impl Fn(usize) -> F + Sync,
downhill: &[isize],
newh: &[u32],
@ -2379,18 +2426,15 @@ pub fn get_multi_rec<F: fmt::Debug + Float + Sync + Into<Compute>>(
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<F: fmt::Debug + Float + Sync + Into<Compute>>(
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<F: fmt::Debug + Float + Sync + Into<Compute>>(
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<F: fmt::Debug + Float + Sync + Into<Compute>>(
#[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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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,

View File

@ -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<usize>,
/// x, y, and z of top left of map (defaults to (0.0, 0.0,
/// CONFIG.sea_level)).
pub focus: Vec3<f64>,
/// 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<f64>,
/// If Some, uses the provided horizon map.
///
/// Defaults to None.
pub horizons: Option<&'a [(Vec<Alt>, Vec<Alt>); 2]>,
/// If Some, uses the provided column samples to determine surface color.
///
/// Defaults to None.
pub samples: Option<&'a [Option<ColumnSample<'a>>]>,
/// 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<i32>) -> 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<f32>,
/// 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<u8>,
/// 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<i32>,
/// 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<Connection>; 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<i32>) -> 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<i32>) -> 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<ColumnSample>]>,
pos: Vec2<i32>,
) -> 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<i32>) -> MapSample,
sample_wpos: impl Fn(Vec2<i32>) -> f32,
// sampler: &WorldSim,
mut write_pixel: impl FnMut(Vec2<usize>, (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
},
}
}

View File

@ -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<usize> = Vec2 {
x: 1024 * 1,
y: 1024 * 1,
const WORLD_SIZE: Vec2<usize> = 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::<Vec<_>>(); */
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::<Vec<_>>(); */
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::<Vec<_>>();
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<u32> { 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<u32> { 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::<Vec<_>>()
@ -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<i32>) -> 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<i32>) -> 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<i32>) -> Option<f32> {
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),
)
};

View File

@ -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<const N: usize>(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<F = f32> = Box<[(f32, F)]>;
/// NOTE: First component is estimated horizon angles at each chunk; second
@ -127,19 +128,6 @@ pub type InverseCdf<F = f32> = Box<[(f32, F)]>;
/// for making shadows volumetric).
pub type HorizonMap<A, H> = (Vec<A>, Vec<H>);
/// Computes the position Vec2 of a SimChunk from an index, where the index was
/// generated by uniform_noise.
pub fn uniform_idx_as_vec2(idx: usize) -> Vec2<i32> {
Vec2::new((idx % WORLD_SIZE.x) as i32, (idx / WORLD_SIZE.x) as i32)
}
/// 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<i32>) -> 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<i32>) -> 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<F: Float + Send>(
map_size_lg: MapSizeLg,
f: impl Fn(usize, Vec2<f64>) -> Option<F> + Sync,
) -> (InverseCdf<F>, 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<F: Float + Send>(
// 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<F: Float + Send>(
/// 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<Item = usize> {
let pos = uniform_idx_as_vec2(posi);
pub fn local_cells(map_size_lg: MapSizeLg, posi: usize) -> impl Clone + Iterator<Item = usize> {
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<Item = usize> {
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<Item = usize> {
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<Item = usize> + '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<Item = usize> + '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<F: Float>(
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<F: Float>(
} 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<T, F>(&self, pos: Vec2<i32>, mut f: F) -> Option<T>
/// - 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<F: Float>(oldh: impl Fn(usize) -> F + Sync) -> BitBox {
pub fn get_oceans<F: Float>(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<F: Float>(oldh: impl Fn(usize) -> F + Sync) -> BitBox {
/// Finds the horizon map for sunlight for the given chunks.
pub fn get_horizon_map<F: Float + Sync, A: Send, H: Send>(
lgain: F,
map_size_lg: MapSizeLg,
bounds: Aabr<i32>,
minh: F,
maxh: F,
@ -416,7 +390,7 @@ pub fn get_horizon_map<F: Float + Sync, A: Send, H: Send>(
};
// 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<F: Float + Sync, A: Send, H: Send>(
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<F: Float + Sync, A: Send, H: Send>(
}
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])
}