diff --git a/Cargo.lock b/Cargo.lock index 311406a162..2f969f766d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4896,11 +4896,13 @@ dependencies = [ "hashbrown", "image", "log 0.4.8", + "num 0.2.1", "num_cpus", "specs", "uvth", "vek 0.10.0", "veloren-common", + "veloren-world", ] [[package]] diff --git a/client/Cargo.toml b/client/Cargo.toml index ad417fb0e3..2b76763b42 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,10 +6,12 @@ edition = "2018" [dependencies] common = { package = "veloren-common", path = "../common", features = ["no-assets"] } +world = { package = "veloren-world", path = "../world" } byteorder = "1.3.2" uvth = "3.1.1" image = "0.22.3" +num = "0.2.0" num_cpus = "1.10.1" log = "0.4.8" specs = "0.15.1" diff --git a/client/src/lib.rs b/client/src/lib.rs index 7341cdd916..d5bde04a28 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -30,9 +30,13 @@ use common::{ vol::RectVolSize, ChatType, }; +// TODO: remove CONFIG dependency by passing CONFIG.sea_level explicitly. +// In general any WORLD dependencies need to go away ASAP... we should see if we +// can pull out map drawing into common somehow. use hashbrown::HashMap; use image::DynamicImage; use log::{error, warn}; +use num::traits::FloatConst; use std::{ net::SocketAddr, sync::Arc, @@ -40,6 +44,10 @@ use std::{ }; use uvth::{ThreadPool, ThreadPoolBuilder}; use vek::*; +use world::{ + sim::{neighbors, Alt}, + CONFIG, +}; // The duration of network inactivity until the player is kicked // @TODO: in the future, this should be configurable on the server @@ -95,7 +103,7 @@ impl Client { entity_package, server_info, time_of_day, - world_map: (map_size, world_map), + world_map, } => { // TODO: Display that versions don't match in Voxygen if server_info.git_hash != common::util::GIT_HASH.to_string() { @@ -121,10 +129,98 @@ impl Client { let entity = state.ecs_mut().apply_entity_package(entity_package); *state.ecs_mut().write_resource() = time_of_day; - assert_eq!(world_map.len(), (map_size.x * map_size.y) as usize); + let map_size = world_map.dimensions; + let max_height = world_map.max_height; + let rgba = world_map.rgba; + assert_eq!(rgba.len(), (map_size.x * map_size.y) as usize); + let [west, east] = world_map.horizons; + let scale_angle = + |a: u8| (a as Alt / 255.0 / ::FRAC_2_PI()).tan(); + let scale_height = + |h: u8| h as Alt * max_height as Alt / 255.0 + CONFIG.sea_level as Alt; + + log::debug!("Preparing image..."); + let unzip_horizons = |(angles, heights): (Vec<_>, Vec<_>)| { + ( + angles.into_iter().map(scale_angle).collect::>(), + heights.into_iter().map(scale_height).collect::>(), + ) + }; + let horizons = [unzip_horizons(west), unzip_horizons(east)]; + + // 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; + map_config.horizons = Some(&horizons); + let rescale_height = + |h: Alt| (h as f32 - map_config.focus.z as f32) / map_config.gain as f32; + let bounds_check = |pos: Vec2| { + pos.reduce_partial_min() >= 0 + && pos.x < map_size.x as i32 + && pos.y < map_size.y as i32 + }; + map_config.generate( + |pos| { + let (rgba, downhill_wpos) = if bounds_check(pos) { + let posi = pos.y as usize * map_size.x as usize + pos.x as usize; + let [r, g, b, a] = rgba[posi].to_le_bytes(); + // Compute downhill. + let downhill = { + let mut best = -1; + let mut besth = a; + // TODO: Fix to work for dynamic WORLD_SIZE (i.e. map_size). + for nposi in neighbors(posi) { + let nbh = rgba[nposi].to_le_bytes()[3]; + if nbh < besth { + besth = nbh; + best = nposi as isize; + } + } + best + }; + let downhill_wpos = if downhill < 0 { + None + } else { + Some( + Vec2::new( + (downhill as usize % map_size.x as usize) as i32, + (downhill as usize / map_size.x as usize) as i32, + ) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + ) + }; + (Rgba::new(r, g, b, a), downhill_wpos) + } else { + (Rgba::zero(), None) + }; + let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let downhill_wpos = downhill_wpos + .unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32)); + let alt = rescale_height(scale_height(rgba.a)); + world::sim::MapSample { + rgb: Rgb::from(rgba), + alt: alt as Alt, + downhill_wpos, + connections: None, + } + }, + |wpos| { + let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); + rescale_height(if bounds_check(pos) { + let posi = pos.y as usize * map_size.x as usize + pos.x as usize; + scale_height(rgba[posi].to_le_bytes()[3]) + } else { + CONFIG.sea_level as Alt + }) + }, + |pos, (r, g, b, a)| { + world_map[pos.y * map_size.x as usize + pos.x] = + u32::from_le_bytes([r, g, b, a]); + }, + ); let mut world_map_raw = vec![0u8; 4 * world_map.len()/*map_size.x * map_size.y*/]; LittleEndian::write_u32_into(&world_map, &mut world_map_raw); - log::debug!("Preparing image..."); let world_map = Arc::new( image::DynamicImage::ImageRgba8({ // Should not fail if the dimensions are correct. diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 5e6fecac33..f2186ebab0 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -25,13 +25,125 @@ pub enum PlayerListUpdate { Alias(u64, String), } +#[derive(Debug, Clone, Serialize, Deserialize)] +/// World map information. Note that currently, we always send the whole thing +/// in one go, but the structure aims to try to provide information as locally +/// as possible, so that in the future we can split up large maps into multiple +/// WorldMapMsg fragments. +/// +/// TODO: Update message format to make fragmentable, allowing us to send more +/// information without running into bandwidth issues. +/// +/// TODO: Add information for rivers (currently, we just prerender them on the +/// server, but this is not a great solution for LoD. The map rendering code is +/// already set up to be able to take advantage of the rivrer rendering being +/// split out, but the format is a little complicated for space reasons and it +/// may take some tweaking to get right, so we avoid sending it for now). +/// +/// TODO: measure explicit compression schemes that might save space, e.g. +/// 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, + /// Max height (used to scale altitudes). + pub max_height: f32, + /// RGB+A; the alpha channel is currently a proxy for altitude. + /// Entries are in the usual chunk order. + pub rgba: Vec, + /// Horizon mapping. This is a variant of shadow mapping that is + /// specifically designed for height maps; it takes advantage of their + /// regular structure (e.g. no holes) to compress all information needed + /// to decide when to cast a sharp shadow into a single nagle, the "horizon + /// angle." This is the smallest angle with the ground at which light can + /// pass through any occluders to reach the chunk, in some chosen + /// horizontal direction. This would not be sufficient for a more + /// complicated 3D structure, but it works for height maps since: + /// + /// 1. they have no gaps, so as soon as light can shine through it will + /// always be able to do so, and + /// 2. we only care about lighting from the top, and only from the east and + /// west (since at a large scale like this we mostly just want to + /// handle variable sunlight; moonlight would present more challenges + /// but we currently have no plans to try to cast accurate shadows in + /// moonlight). + /// + /// Our chosen format is two pairs of vectors, + /// with the first pair representing west-facing light (casting shadows on + /// the left side) and the second representing east-facing light + /// (casting shadows on the east side). + /// + /// The pair of vectors consists of (with each vector in the usual chunk + /// order): + /// + /// * Horizon angle pointing east (1 byte, scaled so 1 unit = 255° / 360). + /// We might consider switching to tangent if that represents the + /// information we care about better. + /// * Approximate (floor) height of maximal occluder. We currently use this + /// to try to deliver some approximation of soft shadows, which isn't that + /// big a deal on the world map but is probably needed in order to ensure + /// smooth transitions between chunks in LoD view. Additionally, when we + /// start using the shadow information to do local lighting on the world + /// map, we'll want a quick way to test where we can go out of shadoow at + /// arbitrary heights (since the player and other entities cajn find + /// themselves far from the ground at times). While this is only an + /// approximation to a proper distance map, hopefully it will give us + /// something that feels reasonable enough for Veloren's style. + /// + /// NOTE: On compression. + /// + /// Horizon mapping has a lot of advantages for height maps (simple, easy to + /// understand, doesn't require any fancy math or approximation beyond + /// precision loss), though it loses a few of them by having to store + /// distance to occluder as well. However, just storing tons + /// and tons of regular shadow maps (153 for a full day cycle, stored at + /// irregular intervals) combined with clever explicit compression and + /// avoiding recording sharp local shadows (preferring retracing for + /// these), yielded a compression rate of under 3 bits per column! Since + /// we likely want to avoid per-column shadows for worlds of the sizes we + /// want, we'd still need to store *some* extra information to create + /// soft shadows, but it would still be nice to try to drive down our + /// size as much as possible given how compressible shadows of height + /// maps seem to be in practice. Therefore, we try to take advantage of the + /// way existing compression algorithms tend to work to see if we can + /// achieve significant gains without doing a lot of custom work. + /// + /// Specifically, since our rays are cast east/west, we expect that for each + /// row, the horizon angles in each direction should be sequences of + /// monotonically increasing values (as chunks approach a tall + /// occluder), followed by sequences of no shadow, repeated + /// until the end of the map. Monotonic sequences and same-byte sequences + /// are usually easy to compress and existing algorithms are more likely + /// to be able to deal with them than jumbled data. If we were to keep + /// both directions in the same vector, off-the-shelf compression would + /// probably be less effective. + /// + /// For related reasons, rather than storing distances as in a standard + /// distance map (which would lead to monotonically *decreaing* values + /// as we approached the occluder from a given direction), we store the + /// estimated *occluder height.* The idea here is that we replace the + /// monotonic sequences with constant sequences, which are extremely + /// straightforward to compress and mostly handled automatically by anything + /// that does run-length encoding (i.e. most off-the-shelf compression + /// algorithms). + /// + /// We still need to benchmark this properly, as there's no guarantee our + /// current compression algorithms will actually work well on this data + /// in practice. It's possible that some other permutation (e.g. more + /// bits reserved for "distance to occluder" in exchange for an even + /// more predictible sequence) would end up compressing better than storing + /// angles, or that we don't need as much precision as we currently have + /// (256 possible angles). + pub horizons: [(Vec, Vec); 2], +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ServerMsg { InitialSync { entity_package: sync::EntityPackage, server_info: ServerInfo, time_of_day: state::TimeOfDay, - world_map: (Vec2, Vec), + world_map: WorldMapMsg, }, PlayerListUpdate(PlayerListUpdate), StateAnswer(Result), diff --git a/server/src/lib.rs b/server/src/lib.rs index 825583c5d9..5c83b273a7 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -28,7 +28,7 @@ use crate::{ use common::{ comp, event::{EventBus, ServerEvent}, - msg::{ClientMsg, ClientState, ServerInfo, ServerMsg}, + msg::{server::WorldMapMsg, ClientMsg, ClientState, ServerInfo, ServerMsg}, net::PostOffice, state::{State, TimeOfDay}, sync::WorldSyncExt, @@ -66,7 +66,7 @@ pub struct Tick(u64); pub struct Server { state: State, world: Arc, - map: Vec, + map: WorldMapMsg, postoffice: PostOffice, @@ -117,7 +117,12 @@ impl Server { #[cfg(not(feature = "worldgen"))] let world = World::generate(settings.world_seed); #[cfg(not(feature = "worldgen"))] - let map = vec![0]; + let map = WorldMapMsg { + dimensions: Vec2::new(1, 1), + max_height: 1.0, + rgba: vec![0], + horizons: [(vec![0], vec![0]), (vec![0], vec![0])], + }; #[cfg(feature = "worldgen")] let spawn_point = { @@ -475,7 +480,7 @@ impl Server { .create_entity_package(entity, None, None, None), server_info: self.server_info.clone(), time_of_day: *self.state.ecs().read_resource(), - world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()), + world_map: self.map.clone(), }); log::debug!("Done initial sync with client."); diff --git a/world/examples/city.rs b/world/examples/city.rs index e98b379220..0331c0ddaf 100644 --- a/world/examples/city.rs +++ b/world/examples/city.rs @@ -30,6 +30,6 @@ fn main() { } } - win.update_with_buffer(&buf).unwrap(); + win.update_with_buffer_size(&buf, W, H).unwrap(); } } diff --git a/world/examples/turb.rs b/world/examples/turb.rs index 302306527e..8249feba9c 100644 --- a/world/examples/turb.rs +++ b/world/examples/turb.rs @@ -34,7 +34,7 @@ fn main() { } } - win.update_with_buffer(&buf).unwrap(); + win.update_with_buffer_size(&buf, W, H).unwrap(); _time += 1.0 / 60.0; } diff --git a/world/examples/view.rs b/world/examples/view.rs index eab8ef1398..330338fbd8 100644 --- a/world/examples/view.rs +++ b/world/examples/view.rs @@ -71,6 +71,6 @@ fn main() { scale -= 6; } - win.update_with_buffer(&buf).unwrap(); + win.update_with_buffer_size(&buf, W, H).unwrap(); } } diff --git a/world/examples/water.rs b/world/examples/water.rs index 4f6696b152..5ceff64654 100644 --- a/world/examples/water.rs +++ b/world/examples/water.rs @@ -1,8 +1,13 @@ use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use rayon::prelude::*; use std::{f64, io::Write, path::PathBuf, time::SystemTime}; use vek::*; use veloren_world::{ - sim::{self, MapConfig, MapDebug, WorldOpts, WORLD_SIZE}, + sim::{ + self, get_horizon_map, uniform_idx_as_vec2, vec2_as_uniform_idx, MapConfig, MapDebug, + MapSample, WorldOpts, WORLD_SIZE, + }, + util::Sampler, World, CONFIG, }; @@ -18,26 +23,92 @@ fn main() { let map_file = // "map_1575990726223.bin"; // "map_1575987666972.bin"; - "map_1576046079066.bin"; + // "map_1576046079066.bin"; + "map_1579539133272.bin"; let mut _map_file = PathBuf::from("./maps"); _map_file.push(map_file); let world = World::generate(5284, WorldOpts { seed_elements: false, + world_file: sim::FileOpts::LoadAsset(veloren_world::sim::DEFAULT_WORLD_MAP.into()), // world_file: sim::FileOpts::Load(_map_file), - world_file: sim::FileOpts::Save, + // world_file: sim::FileOpts::Save, ..WorldOpts::default() }); + log::info!("Sampling data..."); + let sampler = world.sim(); + + let samples_data = { + let column_sample = world.sample_columns(); + (0..WORLD_SIZE.product()) + .into_par_iter() + .map(|posi| { + column_sample + .get(uniform_idx_as_vec2(posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) + }) + .collect::>() + .into_boxed_slice() + }; + let refresh_map_samples = |config: &MapConfig| { + (0..WORLD_SIZE.product()) + .into_par_iter() + .map(|posi| config.sample_pos(sampler, uniform_idx_as_vec2(posi))) + .collect::>() + .into_boxed_slice() + }; + let get_map_sample = |map_samples: &[MapSample], pos: Vec2| { + if pos.reduce_partial_min() >= 0 + && pos.x < WORLD_SIZE.x as i32 + && pos.y < WORLD_SIZE.y as i32 + { + map_samples[vec2_as_uniform_idx(pos)].clone() + } else { + MapSample { + alt: 0.0, + rgb: Rgb::new(0, 0, 0), + connections: None, + downhill_wpos: (pos + 1) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + } + } + }; + + let refresh_horizons = |lgain, is_basement, is_water| { + get_horizon_map( + lgain, + Aabr { + min: Vec2::zero(), + max: WORLD_SIZE.map(|e| e as i32), + }, + CONFIG.sea_level as f64, + (CONFIG.sea_level + sampler.max_height) as f64, + |posi| { + let sample = sampler.get(uniform_idx_as_vec2(posi)).unwrap(); + if is_basement { + sample.alt as f64 + } else { + sample.basement as f64 + } + .max(if is_water { + sample.water_alt as f64 + } else { + -f64::INFINITY + }) + }, + |a| a, + |h| h, + /* |[al, ar]| [al, ar], + * |[hl, hr]| [hl, hr], */ + ) + .ok() + }; let mut win = minifb::Window::new("World Viewer", W, H, minifb::WindowOptions::default()).unwrap(); - let sampler = world.sim(); - let mut focus = Vec3::new(0.0, 0.0, CONFIG.sea_level as f64); // Altitude is divided by gain and clamped to [0, 1]; thus, decreasing gain // makes smaller differences in altitude appear larger. - let mut gain = CONFIG.mountain_scale; + 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; @@ -50,7 +121,7 @@ fn main() { // // "In world space the x-axis will be pointing east, the y-axis up and the // z-axis will be pointing south" - let mut light_direction = Vec3::new(-0.8, -1.0, 0.3); + let mut light_direction = Vec3::new(-/*0.8*/1.3, -1.0, 0.3); let mut is_basement = false; let mut is_water = true; @@ -58,6 +129,11 @@ fn main() { let mut is_temperature = true; let mut is_humidity = true; + let mut horizons = refresh_horizons(lgain, 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 { dimensions: Vec2::new(W, H), @@ -66,6 +142,8 @@ fn main() { lgain, scale, light_direction, + horizons: horizons.as_ref(), /* .map(|(a, b)| (&**a, &**b)) */ + samples, is_basement, is_water, @@ -75,19 +153,30 @@ fn main() { is_debug: true, }; + if samples_changed { + map_samples = refresh_map_samples(&config); + }; + let mut buf = vec![0; W * H]; let MapDebug { rivers, lakes, oceans, quads, - } = config.generate(sampler, |pos, (r, g, b, a)| { - let i = pos.x; - let j = pos.y; - buf[j * W + i] = u32::from_le_bytes([b, g, r, a]); - }); + } = config.generate( + |pos| get_map_sample(&map_samples, pos), + |pos| config.sample_wpos(sampler, pos), + |pos, (r, g, b, a)| { + let i = pos.x; + let j = pos.y; + buf[j * W + i] = u32::from_le_bytes([b, g, r, a]); + }, + ); if win.is_key_down(minifb::Key::F4) { + // Feedback is important since on large maps it can be hard to tell if the + // keypress registered or not. + println!("Taking screenshot..."); if let Some(len) = (W * H) .checked_mul(scale as usize) .and_then(|acc| acc.checked_mul(scale as usize)) @@ -100,11 +189,15 @@ fn main() { ..config }; let mut buf = vec![0u8; 4 * len]; - config.generate(sampler, |pos, (r, g, b, a)| { - let i = pos.x; - let j = pos.y; - (&mut buf[(j * x + i) * 4..]).write(&[r, g, b, a]).unwrap(); - }); + config.generate( + |pos| get_map_sample(&map_samples, pos), + |pos| config.sample_wpos(sampler, pos), + |pos, (r, g, b, a)| { + let i = pos.x; + let j = pos.y; + (&mut buf[(j * x + i) * 4..]).write(&[r, g, b, a]).unwrap(); + }, + ); // TODO: Justify fits in u32. let world_map = image::RgbaImage::from_raw(x as u32, y as u32, buf) .expect("Image dimensions must be valid"); @@ -161,18 +254,39 @@ fn main() { let is_camera = win.is_key_down(minifb::Key::C); 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)); } if win.is_key_down(minifb::Key::H) { is_humidity ^= true; + samples_changed = true; } if win.is_key_down(minifb::Key::T) { is_temperature ^= true; + samples_changed = true; } 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)); } if win.is_key_down(minifb::Key::L) { - is_shaded ^= true; + if is_camera { + // TODO: implement removing horizon mapping. + horizons = if horizons.is_some() { + None + } else { + refresh_horizons(lgain, is_basement, is_water) + }; + samples_changed = true; + } else { + is_shaded ^= true; + samples_changed = true; + } + } + if win.is_key_down(minifb::Key::M) { + samples = samples.xor(Some(&*samples_data)); + samples_changed = true; } if win.is_key_down(minifb::Key::W) { if is_camera { @@ -206,6 +320,8 @@ fn main() { if is_camera { if (lgain * 2.0).is_normal() { lgain *= 2.0; + horizons = + horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); } } else { gain += 64.0; @@ -215,6 +331,8 @@ fn main() { if is_camera { if (lgain / 2.0).is_normal() { lgain /= 2.0; + horizons = + horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); } } else { gain = (gain - 64.0).max(64.0); @@ -223,6 +341,7 @@ fn main() { if win.is_key_down(minifb::Key::R) { if is_camera { focus.z += spd * scale; + samples_changed = true; } else { if (scale * 2.0).is_normal() { scale *= 2.0; @@ -232,6 +351,7 @@ fn main() { if win.is_key_down(minifb::Key::F) { if is_camera { focus.z -= spd * scale; + samples_changed = true; } else { if (scale / 2.0).is_normal() { scale /= 2.0; @@ -239,6 +359,6 @@ fn main() { } } - win.update_with_buffer(&buf).unwrap(); + win.update_with_buffer_size(&buf, W, H).unwrap(); } } diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index c7a8f1babf..dcab7e5906 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -122,7 +122,7 @@ impl<'a> ColumnGen<'a> { } } -fn river_spline_coeffs( +pub fn river_spline_coeffs( // _sim: &WorldSim, chunk_pos: Vec2, spline_derivative: Vec2, @@ -145,7 +145,7 @@ fn river_spline_coeffs( /// 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). -fn quadratic_nearest_point( +pub fn quadratic_nearest_point( spline: &Vec3>, point: Vec2, ) -> Option<(f64, Vec2, f64)> { diff --git a/world/src/config.rs b/world/src/config.rs index dffd712f7d..5f8914d6e1 100644 --- a/world/src/config.rs +++ b/world/src/config.rs @@ -64,5 +64,5 @@ pub const CONFIG: Config = Config { river_roughness: 0.06125, river_max_width: 2.0, river_min_height: 0.25, - river_width_to_depth: 1.0, + river_width_to_depth: 8.0, }; diff --git a/world/src/sim/map.rs b/world/src/sim/map.rs index 566a377ed2..311ed32d6e 100644 --- a/world/src/sim/map.rs +++ b/world/src/sim/map.rs @@ -1,12 +1,16 @@ use crate::{ - sim::{RiverKind, WorldSim, WORLD_SIZE}, + 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, + }, CONFIG, }; use common::{terrain::TerrainChunkSize, vol::RectVolSize}; -use std::{f32, f64}; +use std::{f32, f64, iter}; use vek::*; -pub struct MapConfig { +pub struct MapConfig<'a> { /// Dimensions of the window being written to. Defaults to WORLD_SIZE. pub dimensions: Vec2, /// x, y, and z of top left of map (defaults to (0.0, 0.0, @@ -43,6 +47,14 @@ pub struct MapConfig { /// /// Defaults to (-0.8, -1.0, 0.3). pub light_direction: Vec3, + /// If Some, uses the provided horizon map. + /// + /// Defaults to None. + pub horizons: Option<&'a [(Vec, Vec); 2]>, + /// If Some, uses the provided column samples to determine surface color. + /// + /// Defaults to None. + pub samples: Option<&'a [Option>]>, /// If true, only the basement (bedrock) is used for altitude; otherwise, /// the surface is used. /// @@ -81,7 +93,7 @@ pub struct MapDebug { pub oceans: u32, } -impl Default for MapConfig { +impl<'a> Default for MapConfig<'a> { fn default() -> Self { let dimensions = WORLD_SIZE; Self { @@ -90,7 +102,9 @@ impl Default for MapConfig { 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(-0.8, -1.0, 0.3), + light_direction: Vec3::new(-1.2, -1.0, 0.8), + horizons: None, + samples: None, is_basement: false, is_water: true, @@ -102,15 +116,291 @@ impl Default for MapConfig { } } -impl MapConfig { +/// Connection kind (per edge). Currently just supports rivers, but may be +/// extended to support paths or at least one other kind of connection. +#[derive(Clone, Copy, Debug)] +pub enum ConnectionKind { + /// Connection forms a visible river. + River, +} + +/// Map connection (per edge). +#[derive(Clone, Copy, Debug)] +pub struct Connection { + /// The kind of connection this is (e.g. river or path). + pub kind: ConnectionKind, + /// Assumed to be the "b" part of a 2d quadratic function. + pub spline_derivative: Vec2, + /// Width of the connection. + pub width: f32, +} + +/// Per-chunk data the map needs to be able to sample in order to correctly +/// render. +#[derive(Clone, Debug)] +pub struct MapSample { + /// the base RGB color for a particular map pixel using the current settings + /// (i.e. the color *without* lighting). + pub rgb: Rgb, + /// Surface altitude information + /// (correctly reflecting settings like is_basement and is_water) + pub alt: f64, + /// Downhill chunk (may not be meaningful on ocean tiles, or at least edge + /// tiles) + pub downhill_wpos: Vec2, + /// Connection information about any connections to/from this chunk (e.g. + /// rivers). + /// + /// Connections at each index correspond to the same index in + /// NEIGHBOR_DELTA. + pub connections: Option<[Option; 8]>, +} + +impl<'a> MapConfig<'a> { + /// A sample function that grabs the connections at a chunk. + /// + /// Currently this just supports rivers, but ideally it can be extended past + /// that. + /// + /// A sample function that grabs surface altitude at a column. + /// (correctly reflecting settings like is_basement and is_water). + /// + /// The altitude produced by this function at a column corresponding to a + /// particular chunk should be identical to the altitude produced by + /// sample_pos at that chunk. + /// + /// You should generally pass a closure over this function into generate + /// when constructing a map for the first time. + /// However, if repeated construction is needed, or alternate base colors + /// are to be used for some reason, one should pass a custom function to + /// generate instead (e.g. one that just looks up the height in a cached + /// array). + pub fn sample_wpos(&self, sampler: &WorldSim, wpos: Vec2) -> f32 { + let MapConfig { + focus, + gain, + + is_basement, + is_water, + .. + } = *self; + + (sampler + .get_wpos(wpos) + .map(|s| { + if is_basement { s.basement } else { s.alt }.max(if is_water { + s.water_alt + } else { + -f32::INFINITY + }) + }) + .unwrap_or(CONFIG.sea_level) + - focus.z as f32) + / gain as f32 + } + + /// Samples a MapSample at a chunk. + /// + /// You should generally pass a closure over this function into generate + /// when constructing a map for the first time. + /// However, if repeated construction is needed, or alternate base colors + /// are to be used for some reason, one should pass a custom function to + /// generate instead (e.g. one that just looks up the color in a cached + /// array). + pub fn sample_pos(&self, sampler: &WorldSim, pos: Vec2) -> MapSample { + let MapConfig { + focus, + gain, + samples, + + is_basement, + is_water, + is_shaded, + is_temperature, + is_humidity, + // is_debug, + .. + } = *self; + + let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64; + + let ( + chunk_idx, + alt, + basement, + water_alt, + humidity, + temperature, + downhill, + river_kind, + spline_derivative, + ) = sampler + .get(pos) + .map(|sample| { + ( + Some(vec2_as_uniform_idx(pos)), + sample.alt, + sample.basement, + sample.water_alt, + sample.humidity, + sample.temp, + sample.downhill, + sample.river.river_kind, + sample.river.spline_derivative, + ) + }) + .unwrap_or(( + None, + CONFIG.sea_level, + CONFIG.sea_level, + CONFIG.sea_level, + 0.0, + 0.0, + None, + None, + Vec2::zero(), + )); + + let humidity = humidity.min(1.0).max(0.0); + let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5; + let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let column_rgb = samples + .and_then(|samples| { + chunk_idx + .and_then(|chunk_idx| samples.get(chunk_idx)) + .map(Option::as_ref) + .flatten() + }) + .map(|sample| { + // TODO: Eliminate the redundancy between this and the block renderer. + let grass_depth = (1.5 + 2.0 * sample.chaos).min(alt - basement); + let wposz = if is_basement { basement } else { alt }; + if is_basement && wposz < alt - grass_depth { + Lerp::lerp( + sample.sub_surface_color, + sample.stone_col.map(|e| e as f32 / 255.0), + (alt - grass_depth - wposz as f32) * 0.15, + ) + .map(|e| e as f64) + } else { + Lerp::lerp( + sample.sub_surface_color, + sample.surface_color, + ((wposz as f32 - (alt - grass_depth)) / grass_depth).powf(0.5), + ) + .map(|e| e as f64) + } + }); + + let downhill_wpos = downhill + .map(|downhill_pos| downhill_pos) + .unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32)); + let alt = if is_basement { basement } else { alt }; + + let true_water_alt = (alt.max(water_alt) as f64 - focus.z) / gain as f64; + let true_alt = (alt as f64 - focus.z) / gain as f64; + let water_depth = (true_water_alt - true_alt).min(1.0).max(0.0); + let alt = true_alt.min(1.0).max(0.0); + + let water_color_factor = 2.0; + let g_water = 32.0 * water_color_factor; + let b_water = 64.0 * water_color_factor; + let column_rgb = column_rgb.unwrap_or(Rgb::new( + if is_shaded || is_temperature { + 1.0 + } else { + 0.0 + }, + if is_shaded { 1.0 } else { alt }, + if is_shaded || is_humidity { 1.0 } else { 0.0 }, + )); + let mut connections = [None; 8]; + let mut has_connections = false; + // TODO: Support non-river connections. + // TODO: Support multiple connections. + let river_width = river_kind.map(|river| match river { + RiverKind::River { cross_section } => cross_section.x, + RiverKind::Lake { .. } | RiverKind::Ocean => TerrainChunkSize::RECT_SIZE.x as f32, + }); + if let (Some(river_width), true) = (river_width, is_water) { + let downhill_pos = downhill_wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32); + NEIGHBOR_DELTA + .iter() + .zip((&mut connections).iter_mut()) + .filter(|&(&offset, _)| downhill_pos - pos == Vec2::from(offset)) + .for_each(|(_, connection)| { + has_connections = true; + *connection = Some(Connection { + kind: ConnectionKind::River, + spline_derivative, + width: river_width, + }); + }); + }; + let rgb = match (river_kind, (is_water, true_alt >= true_sea_level)) { + (_, (false, _)) | (None, (_, true)) | (Some(RiverKind::River { .. }), _) => { + let (r, g, b) = ( + (column_rgb.r + * if is_temperature { + temperature as f64 + } else { + column_rgb.r + }) + .sqrt(), + column_rgb.g, + (column_rgb.b + * if is_humidity { + humidity as f64 + } else { + column_rgb.b + }) + .sqrt(), + ); + Rgb::new((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8) + }, + (None, _) | (Some(RiverKind::Lake { .. }), _) | (Some(RiverKind::Ocean), _) => { + Rgb::new( + 0, + ((g_water - water_depth * g_water) * 1.0) as u8, + ((b_water - water_depth * b_water) * 1.0) as u8, + ) + }, + }; + + MapSample { + rgb, + alt: if is_water { + true_alt.max(true_water_alt) + } else { + true_alt + }, + downhill_wpos, + connections: if has_connections { + Some(connections) + } else { + None + }, + } + } + /// Generates a map image using the specified settings. Note that it will /// write from left to write from (0, 0) to dimensions - 1, inclusive, /// with 4 1-byte color components provided as (r, g, b, a). It is up /// to the caller to provide a function that translates this information /// into the correct format for a buffer and writes to it. + /// + /// sample_pos is a function that, given a chunk position, returns enough + /// information about the chunk to attempt to render it on the map. + /// When in doubt, try using `MapConfig::sample_pos` for this. + /// + /// sample_wpos is a simple function that, given a *column* position, + /// returns the approximate altitude at that column. When in doubt, try + /// using `MapConfig::sample_wpos` for this. pub fn generate( &self, - sampler: &WorldSim, + sample_pos: impl Fn(Vec2) -> MapSample, + sample_wpos: impl Fn(Vec2) -> f32, + // sampler: &WorldSim, mut write_pixel: impl FnMut(Vec2, (u8, u8, u8, u8)), ) -> MapDebug { let MapConfig { @@ -120,23 +410,29 @@ impl MapConfig { lgain, scale, light_direction, + horizons, - is_basement, - is_water, is_shaded, - is_temperature, - is_humidity, - is_debug, + // 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 /*mut */quads = [[0u32; QUADRANTS]; QUADRANTS]; + let /*mut */rivers = 0u32; + let /*mut */lakes = 0u32; + let /*mut */oceans = 0u32; let focus_rect = Vec2::from(focus); - let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64; + + let chunk_size = TerrainChunkSize::RECT_SIZE.map(|e| e as f64); (0..dimensions.y * dimensions.x) .into_iter() @@ -144,81 +440,148 @@ impl MapConfig { let i = chunk_idx % dimensions.x as usize; let j = chunk_idx / dimensions.x as usize; - let pos = - (focus_rect + Vec2::new(i as f64, j as f64) * scale).map(|e: f64| e as i32); + 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 (alt, basement, water_alt, humidity, temperature, downhill, river_kind) = - sampler - .get(pos) - .map(|sample| { - ( - sample.alt, - sample.basement, - sample.water_alt, - sample.humidity, - sample.temp, - sample.downhill, - sample.river.river_kind, + 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(), ) - }) - .unwrap_or(( - CONFIG.sea_level, - CONFIG.sea_level, - CONFIG.sea_level, - 0.0, - 0.0, - None, - None, - )); - let humidity = humidity.min(1.0).max(0.0); - let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5; - let pos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); - let downhill_pos = (downhill - .map(|downhill_pos| downhill_pos) - .unwrap_or(pos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32)) - - pos) - + pos; - let downhill_alt = sampler - .get_wpos(downhill_pos) - .map(|s| if is_basement { s.basement } else { s.alt }) - .unwrap_or(CONFIG.sea_level); - let alt = if is_basement { basement } else { alt }; - let cross_pos = pos - + ((downhill_pos - pos) + .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 = sampler - .get_wpos(cross_pos) - .map(|s| if is_basement { s.basement } else { s.alt }) - .unwrap_or(CONFIG.sea_level); + 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_pos.x - pos.x) as f64, - (downhill_alt - alt) as f64 * lgain, - (downhill_pos.y - pos.y) as f64, + (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 - pos.x) as f64, - (cross_alt - alt) as f64 * lgain, - (cross_pos.y - pos.y) as f64, + (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(); - let light = (surface_normal.dot(light) + 1.0) / 2.0; - let light = (light * 0.9) + 0.1; - let true_water_alt = (alt.max(water_alt) as f64 - focus.z) / gain as f64; - let true_alt = (alt as f64 - focus.z) / gain as f64; - let water_depth = (true_water_alt - true_alt).min(1.0).max(0.0); - let water_alt = true_water_alt.min(1.0).max(0.0); - let alt = true_alt.min(1.0).max(0.0); - if is_debug { + // 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) }; @@ -237,62 +600,84 @@ impl MapConfig { }, 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; + if angle != 0.0 && light_direction.x != 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 water_color_factor = 2.0; - let g_water = 32.0 * water_color_factor; - let b_water = 64.0 * water_color_factor; - let rgb = match (river_kind, (is_water, true_alt >= true_sea_level)) { - (_, (false, _)) | (None, (_, true)) => { - let (r, g, b) = ( - (if is_shaded { alt } else { alt } - * if is_temperature { - temperature as f64 - } else if is_shaded { - alt - } else { - 0.0 - }) - .sqrt(), - if is_shaded { 0.4 + (alt * 0.6) } else { alt }, - (if is_shaded { alt } else { alt } - * if is_humidity { - humidity as f64 - } else if is_shaded { - alt - } else { - 0.0 - }) - .sqrt(), - ); - let light = if is_shaded { light } else { 1.0 }; - ( - (r * light * 255.0) as u8, - (g * light * 255.0) as u8, - (b * light * 255.0) as u8, - ) - }, - (Some(RiverKind::Ocean), _) => ( - 0, - ((g_water - water_depth * g_water) * 1.0) as u8, - ((b_water - water_depth * b_water) * 1.0) as u8, - ), - (Some(RiverKind::River { .. }), _) => ( - 0, - g_water as u8 + (alt * (127.0 - g_water)) as u8, - b_water as u8 + (alt * (255.0 - b_water)) as u8, - ), - (None, _) | (Some(RiverKind::Lake { .. }), _) => ( - 0, - (((g_water + water_alt * (127.0 - 32.0)) + (-water_depth * g_water)) * 1.0) - as u8, - (((b_water + water_alt * (255.0 - b_water)) + (-water_depth * b_water)) - * 1.0) as u8, - ), - }; - - let rgba = (rgb.0, rgb.1, rgb.2, (255.0 * alt.max(water_alt)) as u8); - + let rgba = (rgb.r, rgb.g, rgb.b, 255); write_pixel(Vec2::new(i, j), rgba); }); diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index feb3643eb5..c177885b3b 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -14,12 +14,12 @@ pub use self::{ get_rivers, mrec_downhill, Alt, RiverData, RiverKind, }, location::Location, - map::{MapConfig, MapDebug}, + map::{MapConfig, MapDebug, MapSample}, settlement::Settlement, util::{ - cdf_irwin_hall, downhill, get_oceans, local_cells, map_edge_factor, neighbors, - uniform_idx_as_vec2, uniform_noise, uphill, vec2_as_uniform_idx, InverseCdf, ScaleBias, - NEIGHBOR_DELTA, + 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, }, }; @@ -33,6 +33,7 @@ use crate::{ }; use common::{ assets, + msg::server::WorldMapMsg, terrain::{BiomeKind, TerrainChunkSize}, vol::RectVolSize, }; @@ -41,7 +42,7 @@ use noise::{ BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti, Seedable, SuperSimplex, Worley, }; -use num::{Float, Signed}; +use num::{traits::FloatConst, Float, Signed}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaChaRng; use rayon::prelude::*; @@ -1101,9 +1102,9 @@ impl WorldSim { ) }; let flux_old = get_multi_drainage(&mstack, &mrec, &*mwrec, boundary_len); - let flux_rivers = get_drainage(&water_alt_pos, &dh, 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_rivers = flux_old.clone(); + let flux_rivers = flux_old.clone(); let water_height_initial = |chunk_idx| { let indirection_idx = indirection[chunk_idx]; @@ -1307,17 +1308,75 @@ impl WorldSim { /// Draw a map of the world based on chunk information. Returns a buffer of /// u32s. - pub fn get_map(&self) -> Vec { + pub fn get_map(&self) -> WorldMapMsg { + let mut map_config = MapConfig::default(); + map_config.lgain = 1.0; + // Build a horizon map. + let scale_angle = + |angle: Alt| (angle.atan() * ::FRAC_2_PI() * 255.0).floor() as u8; + let scale_height = |height: Alt| { + ((height - CONFIG.sea_level as Alt) * 255.0 / self.max_height as Alt).floor() as u8 + }; + let horizons = get_horizon_map( + map_config.lgain, + Aabr { + min: Vec2::zero(), + max: WORLD_SIZE.map(|e| e as i32), + }, + CONFIG.sea_level as Alt, + (CONFIG.sea_level + self.max_height) as Alt, + |posi| { + let chunk = &self.chunks[posi]; + chunk.alt.max(chunk.water_alt) as Alt + }, + |a| scale_angle(a), + |h| scale_height(h), + ) + .unwrap(); + + let samples_data = { + let column_sample = ColumnGen::new(self); + (0..WORLD_SIZE.product()) + .into_par_iter() + .map(|posi| { + column_sample.get( + uniform_idx_as_vec2(posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + ) + }) + .collect::>() + .into_boxed_slice() + }; + let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y]; // TODO: Parallelize again. - MapConfig { + let config = MapConfig { gain: self.max_height, - ..MapConfig::default() + samples: Some(&samples_data), + is_shaded: false, + ..map_config + }; + + config.generate( + |pos| config.sample_pos(self, pos), + |pos| config.sample_wpos(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( + self, + pos.map(|e| e as i32) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + ); + let a = (alt.min(1.0).max(0.0) * 255.0) as u8; + + v[pos.y * WORLD_SIZE.x + pos.x] = u32::from_le_bytes([r, g, b, a]); + }, + ); + WorldMapMsg { + dimensions: WORLD_SIZE.map(|e| e as u32), + max_height: self.max_height, + rgba: v, + horizons, } - .generate(&self, |pos, (r, g, b, a)| { - v[pos.y * WORLD_SIZE.x + pos.x] = u32::from_le_bytes([r, g, b, a]); - }); - v } /// Prepare the world for simulation diff --git a/world/src/sim/util.rs b/world/src/sim/util.rs index fa138c40bf..3d7e6cfd63 100644 --- a/world/src/sim/util.rs +++ b/world/src/sim/util.rs @@ -294,6 +294,52 @@ pub fn downhill( .into_boxed_slice() } +/* /// Bilinear interpolation. +/// +/// Linear interpolation in both directions (i.e. quadratic interpolation). +fn get_interpolated_bilinear(&self, pos: Vec2, mut f: F) -> Option + where + T: Copy + Default + Signed + Float + Add + Mul, + F: FnMut(Vec2) -> Option, +{ + // (i) Find downhill for all four points. + // (ii) Compute distance from each downhill point and do linear interpolation on + // their heights. (iii) Compute distance between each neighboring point + // and do linear interpolation on their distance-interpolated + // heights. + + // See http://articles.adsabs.harvard.edu/cgi-bin/nph-iarticle_query?1990A%26A...239..443S&defaultprint=YES&page_ind=0&filetype=.pdf + // + // Note that these are only guaranteed monotone in one dimension; fortunately, + // that is sufficient for our purposes. + let pos = pos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { + e as f64 / sz as f64 + }); + + // Orient the chunk in the direction of the most downhill point of the four. If + // there is no "most downhill" point, then we don't care. + let x0 = pos.map2(Vec2::new(0, 0), |e, q| e.max(0.0) as i32 + q); + let y0 = f(x0)?; + + let x1 = pos.map2(Vec2::new(1, 0), |e, q| e.max(0.0) as i32 + q); + let y1 = f(x1)?; + + let x2 = pos.map2(Vec2::new(0, 1), |e, q| e.max(0.0) as i32 + q); + let y2 = f(x2)?; + + let x3 = pos.map2(Vec2::new(1, 1), |e, q| e.max(0.0) as i32 + q); + let y3 = f(x3)?; + + let z0 = y0 + .mul(1.0 - pos.x.fract() as f32) + .mul(1.0 - pos.y.fract() as f32); + let z1 = y1.mul(pos.x.fract() as f32).mul(1.0 - pos.y.fract() as f32); + let z2 = y2.mul(1.0 - pos.x.fract() as f32).mul(pos.y.fract() as f32); + let z3 = y3.mul(pos.x.fract() as f32).mul(pos.y.fract() as f32); + + Some(z0 + z1 + z2 + z3) +} */ + /// Find all ocean tiles from a height map, using an inductive definition of /// ocean as one of: /// - posi is at the side of the world (map_edge_factor(posi) == 0.0) @@ -335,6 +381,78 @@ pub fn get_oceans(oldh: impl Fn(usize) -> F + Sync) -> BitBox { is_ocean } +/// Finds the horizon map for sunlight for the given chunks. +pub fn get_horizon_map( + lgain: F, + bounds: Aabr, + minh: F, + maxh: F, + h: impl Fn(usize) -> F + Sync, + to_angle: impl Fn(F) -> A + Sync, + to_height: impl Fn(F) -> H + Sync, +) -> Result<[(Vec, Vec); 2], ()> { + let map_size = Vec2::::from(bounds.size()).map(|e| e as usize); + let map_len = map_size.product(); + + // Now, do the raymarching. + let chunk_x = if let Vec2 { x: Some(x), .. } = TerrainChunkSize::RECT_SIZE.map(F::from) { + x + } else { + return Err(()); + }; + // 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 mut angles = Vec::with_capacity(map_len); + let mut heights = Vec::with_capacity(map_len); + (0..map_len) + .into_par_iter() + .map(|posi| { + 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.y as usize >= WORLD_SIZE.x + || wposi.y as usize >= WORLD_SIZE.y + { + return (to_angle(F::zero()), to_height(F::zero())); + } + let posi = vec2_as_uniform_idx(wposi); + // March in the given direction. + let maxdx = maxdx(wposi.x as isize); + let mut slope = F::zero(); + let mut max_height = F::zero(); + let h0 = h(posi); + if h0 >= minh { + let maxdz = maxh - h0; + let posi = posi as isize; + for deltax in 1..maxdx { + let posj = (posi + deltax * dx) as usize; + let deltax = chunk_x * F::from(deltax).unwrap(); + let h_j_est = slope * deltax; + if h_j_est > maxdz { + break; + } + let h_j_act = h(posj) - h0; + if + /* h_j_est - h_j_act <= epsilon */ + h_j_est <= h_j_act { + slope = h_j_act / deltax; + max_height = h_j_act; + } + } + } + let a = slope * lgain; + let h = h0 + max_height; + (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); + Ok([west, east]) +} + /// A 2-dimensional vector, for internal use. type Vector2 = [T; 2]; /// A 3-dimensional vector, for internal use.