mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
parent
30b1d2c642
commit
13b6d4d534
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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((
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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
719
common/src/terrain/map.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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,9 +110,10 @@ 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 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))
|
||||
});
|
||||
|
@ -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::{
|
||||
|
@ -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,8 +896,9 @@ 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))
|
||||
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
|
||||
@ -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,7 +992,8 @@ 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))
|
||||
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();
|
||||
|
||||
@ -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,7 +1410,8 @@ 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))
|
||||
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);
|
||||
@ -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,8 +2461,9 @@ 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))
|
||||
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();
|
||||
@ -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,
|
||||
|
@ -1,181 +1,37 @@
|
||||
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,
|
||||
}
|
||||
|
||||
pub const QUADRANTS: usize = 4;
|
||||
|
||||
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 {
|
||||
/// 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,
|
||||
@ -183,7 +39,7 @@ impl<'a> MapConfig<'a> {
|
||||
is_basement,
|
||||
is_water,
|
||||
..
|
||||
} = *self;
|
||||
} = *config;
|
||||
|
||||
(sampler
|
||||
.get_wpos(wpos)
|
||||
@ -197,21 +53,26 @@ impl<'a> MapConfig<'a> {
|
||||
.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 {
|
||||
/// 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,
|
||||
samples,
|
||||
|
||||
is_basement,
|
||||
is_water,
|
||||
@ -220,7 +81,7 @@ impl<'a> MapConfig<'a> {
|
||||
is_humidity,
|
||||
// is_debug,
|
||||
..
|
||||
} = *self;
|
||||
} = *config;
|
||||
|
||||
let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64;
|
||||
|
||||
@ -240,7 +101,7 @@ impl<'a> MapConfig<'a> {
|
||||
.get(pos)
|
||||
.map(|sample| {
|
||||
(
|
||||
Some(vec2_as_uniform_idx(pos)),
|
||||
Some(vec2_as_uniform_idx(map_size_lg, pos)),
|
||||
sample.alt,
|
||||
sample.basement,
|
||||
sample.water_alt,
|
||||
@ -371,13 +232,11 @@ impl<'a> MapConfig<'a> {
|
||||
);
|
||||
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(
|
||||
(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 {
|
||||
@ -402,311 +261,4 @@ impl<'a> MapConfig<'a> {
|
||||
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).
|
||||
chunk_idx
|
||||
.map(|chunk_idx| neighbors(chunk_idx).chain(iter::once(chunk_idx)))
|
||||
.into_iter()
|
||||
.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))
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,7 +760,8 @@ 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))
|
||||
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)
|
||||
@ -797,7 +815,8 @@ 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))
|
||||
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)
|
||||
@ -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),
|
||||
)
|
||||
};
|
||||
|
@ -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,14 +166,16 @@ 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))
|
||||
(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])
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user