diff --git a/Cargo.lock b/Cargo.lock index a2c3455bdf..ee66f18596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3890,11 +3890,13 @@ dependencies = [ "hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.22.3 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "specs 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", "uvth 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "vek 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "veloren-common 0.5.0", + "veloren-world 0.5.0", ] [[package]] @@ -4021,7 +4023,6 @@ dependencies = [ name = "veloren-world" version = "0.5.0" dependencies = [ - "approx 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "arr_macro 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "bitvec 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/client/Cargo.toml b/client/Cargo.toml index 9d649f0b01..e638dc1251 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 6d7b6f7405..c848803a09 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -29,9 +29,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::warn; +use num::traits::FloatConst; use std::{ net::SocketAddr, sync::Arc, @@ -39,6 +43,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 @@ -94,7 +102,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() { @@ -115,10 +123,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 c517eec665..87bf35c708 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 5d06f7498f..faedb28664 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, @@ -118,7 +118,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 = { @@ -471,7 +476,7 @@ impl Server { .create_entity_package(entity), 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/Cargo.toml b/world/Cargo.toml index 096c988928..4dd4c22b71 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -5,7 +5,6 @@ authors = ["Joshua Barretto "] edition = "2018" [dependencies] -approx = "0.1.1" bincode = "1.2.0" common = { package = "veloren-common", path = "../common" } bitvec = "0.15.2" 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 b86c14d616..e737dc8341 100644 --- a/world/examples/water.rs +++ b/world/examples/water.rs @@ -4,7 +4,8 @@ use std::{f64, io::Write, path::PathBuf, time::SystemTime}; use vek::*; use veloren_world::{ sim::{ - self, get_shadows, uniform_idx_as_vec2, Alt, MapConfig, MapDebug, WorldOpts, WORLD_SIZE, + self, get_horizon_map, uniform_idx_as_vec2, vec2_as_uniform_idx, MapConfig, MapDebug, + MapSample, WorldOpts, WORLD_SIZE, }, util::Sampler, World, CONFIG, @@ -29,8 +30,8 @@ fn main() { 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::LoadAsset(veloren_world::sim::DEFAULT_WORLD_MAP.into()), + // world_file: sim::FileOpts::Load(_map_file), // world_file: sim::FileOpts::Save, ..WorldOpts::default() }); @@ -48,29 +49,57 @@ fn main() { .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_shadows = |light_direction: Vec3, lgain, scale, is_basement, is_water| { - get_shadows( - //Vec3::new(-0.8, 0.3, /*-1.0*/-(1.0 / TerrainChunkSize::RECT_SIZE.x as Alt)), - Vec3::new(light_direction.x, light_direction.z, light_direction.y/* / lgain*/), - lgain, - TerrainChunkSize::RECT_SIZE.x as f64/* * scale*/, - TerrainChunkSize::RECT_SIZE.y as f64/* * scale*/, - Aabr { - min: Vec2::new(0.0, 0.0), // focus.into(), - max: WORLD_SIZE.map(|e| e as f64) * TerrainChunkSize::RECT_SIZE.map(|e| e as f64)/* * scale*//* + focus.into() */, - }, - CONFIG.sea_level as f64, // focus.z, - (CONFIG.sea_level + sampler.max_height) as f64, // (focus.z + self.max_height) as Alt, - |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 }) - }, - ).ok() + 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 = @@ -100,9 +129,11 @@ fn main() { let mut is_temperature = true; let mut is_humidity = true; - let mut shadows = None; //refresh_shadows(light_direction, lgain, scale, is_basement, is_water); + 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), @@ -111,7 +142,7 @@ fn main() { lgain, scale, light_direction, - shadows: shadows.as_deref(), + horizons: horizons.as_ref(), /* .map(|(a, b)| (&**a, &**b)) */ samples, is_basement, @@ -119,23 +150,33 @@ fn main() { is_shaded, is_temperature, is_humidity, - // is_sampled, 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)) @@ -148,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"); @@ -209,39 +254,37 @@ fn main() { let is_camera = win.is_key_down(minifb::Key::C); if win.is_key_down(minifb::Key::B) { is_basement ^= true; - shadows = shadows.and_then(|_| { - refresh_shadows(light_direction, lgain, scale, is_basement, is_water) - }); + 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) { if is_camera { - shadows = match shadows { - Some(_) => None, - None => refresh_shadows(light_direction, lgain, scale, is_basement, is_water), - }; + // TODO: implement removing horizon mapping. } else { is_shaded ^= true; + samples_changed = true; } } if win.is_key_down(minifb::Key::M) { samples = samples.xor(Some(&*samples_data)); - // is_sampled ^= true; + samples_changed = true; } if win.is_key_down(minifb::Key::W) { if is_camera { light_direction.z -= lspd; - shadows = shadows.and_then(|_| { - refresh_shadows(light_direction, lgain, scale, is_basement, is_water) - }); } else { focus.y -= spd * scale; } @@ -249,9 +292,6 @@ fn main() { if win.is_key_down(minifb::Key::A) { if is_camera { light_direction.x -= lspd; - shadows = shadows.and_then(|_| { - refresh_shadows(light_direction, lgain, scale, is_basement, is_water) - }); } else { focus.x -= spd * scale; } @@ -259,9 +299,6 @@ fn main() { if win.is_key_down(minifb::Key::S) { if is_camera { light_direction.z += lspd; - shadows = shadows.and_then(|_| { - refresh_shadows(light_direction, lgain, scale, is_basement, is_water) - }); } else { focus.y += spd * scale; } @@ -269,9 +306,6 @@ fn main() { if win.is_key_down(minifb::Key::D) { if is_camera { light_direction.x += lspd; - shadows = shadows.and_then(|_| { - refresh_shadows(light_direction, lgain, scale, is_basement, is_water) - }); } else { focus.x += spd * scale; } @@ -280,9 +314,8 @@ fn main() { if is_camera { if (lgain * 2.0).is_normal() { lgain *= 2.0; - shadows = shadows.and_then(|_| { - refresh_shadows(light_direction, lgain, scale, is_basement, is_water) - }); + horizons = + horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); } } else { gain += 64.0; @@ -292,9 +325,8 @@ fn main() { if is_camera { if (lgain / 2.0).is_normal() { lgain /= 2.0; - shadows = shadows.and_then(|_| { - refresh_shadows(light_direction, lgain, scale, is_basement, is_water) - }); + horizons = + horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water)); } } else { gain = (gain - 64.0).max(64.0); @@ -303,26 +335,24 @@ 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; - // shadows = refresh_shadows(light_direction, lgain, scale, - // is_basement); } } } 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; - // shadows = refresh_shadows(light_direction, lgain, scale, - // is_basement); } } } - 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/sim/map.rs b/world/src/sim/map.rs index 17417a8a09..34a8f6365f 100644 --- a/world/src/sim/map.rs +++ b/world/src/sim/map.rs @@ -1,10 +1,13 @@ use crate::{ - column::ColumnSample, - sim::{vec2_as_uniform_idx, Alt, 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<'a> { @@ -44,10 +47,10 @@ pub struct MapConfig<'a> { /// /// Defaults to (-0.8, -1.0, 0.3). pub light_direction: Vec3, - /// If Some, uses the provided shadow map. + /// If Some, uses the provided horizon map. /// /// Defaults to None. - pub shadows: Option<&'a [Alt]>, + pub horizons: Option<&'a [(Vec, Vec); 2]>, /// If Some, uses the provided column samples to determine surface color. /// /// Defaults to None. @@ -100,7 +103,7 @@ impl<'a> Default for MapConfig<'a> { 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), - shadows: None, + horizons: None, samples: None, is_basement: false, @@ -113,15 +116,291 @@ impl<'a> Default for MapConfig<'a> { } } +/// 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 { @@ -131,29 +410,29 @@ impl<'a> MapConfig<'a> { lgain, scale, light_direction, - shadows, - samples, + 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, light_direction.z); - // let light_direction = Vec3::new(light_direction.x * lgain, light_direction.y, - // light_direction.z * lgain); + 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 column_sample = ColumnGen::new(sampler); + 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() @@ -161,101 +440,138 @@ impl<'a> MapConfig<'a> { 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 ( - chunk_idx, - alt, - basement, - water_alt, - humidity, - temperature, - downhill, - river_kind, - ) = 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, - ) - }) - .unwrap_or(( - None, - 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 column_rgb = samples - .and_then(|samples| { - chunk_idx - .and_then(|chunk_idx| samples.get(chunk_idx)) - .map(Option::as_ref) - .flatten() - }) - .map(|sample| { - if is_basement { - sample.stone_col.map(|e| e as f64 / 255.0) - } else { - sample.surface_color.map(|e| e as f64) - } - }); - /*let column_rgb = if is_sampled { - column_sample.get(pos) - .map(|sample| if is_basement { - sample.stone_col.map(|e| e as f64 / 255.0) - } else { - sample.surface_color.map(|e| e as f64) - }) + 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 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) + }; + + 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 = 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(); @@ -263,15 +579,9 @@ impl<'a> MapConfig<'a> { // 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) }; @@ -290,143 +600,84 @@ impl<'a> MapConfig<'a> { }, 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(alt as f64); + + 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(0.45, 0.45, 0.45); + // 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 shade_frac = shadows - .and_then(|shadows| chunk_idx.and_then(|chunk_idx| shadows.get(chunk_idx))) - .copied() - .map(|e| e as f64) - .unwrap_or(alt); - let water_color_factor = 2.0; - let g_water = 32.0 - * water_color_factor - * if is_shaded { - 0.2 + shade_frac * 0.8 - } else { - 1.0 - }; - let b_water = 64.0 - * water_color_factor - * if is_shaded { - 0.2 + shade_frac * 0.8 - } else { - 1.0 - }; - let column_rgb = column_rgb.unwrap_or(Rgb::new( - if is_shaded { shade_frac * 0.6 } else { alt }, - if is_shaded { - 0.4 + (shade_frac * 0.6) - } else { - alt - }, - if is_shaded { shade_frac * 0.6 } else { alt }, - )); - let rgba = match (river_kind, (is_water, true_alt >= true_sea_level)) { - (_, (false, _)) | (None, (_, true)) => { - let (r, g, b) = ( - (column_rgb.r/*if is_shaded { shade_frac * 0.6 } else { alt }*/ - * if is_temperature { - temperature as f64 - } else if is_shaded { - if samples.is_some() { - // column_rgb.r - 0.2 + shade_frac * 0.8 - } else { - shade_frac * 0.6 - } - } else { - if samples.is_some() { - alt - } else { - 0.0 - } - }) - .sqrt(), - (column_rgb.g - * if is_shaded { - if samples.is_some() { - // column_rgb.g - 0.2 + shade_frac * 0.8 - } else { - 0.4 + shade_frac * 0.6 - } - } else { - alt - }) - .sqrt(), - (column_rgb.b/*if is_shaded { shade_frac * 0.6 } else { alt }*/ - * if is_humidity { - humidity as f64 - } else if is_shaded { - if samples.is_some() { - // column_rgb.b - 0.2 + shade_frac * 0.8 - } else { - shade_frac * 0.6 - } - } else { - if samples.is_some() { - 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, - 255, - ) - }, - (Some(RiverKind::Ocean), _) => ( - 0, - ((g_water - water_depth * g_water) * 1.0) as u8, - ((b_water - water_depth * b_water) * 1.0) as u8, - 255, - ), - (Some(RiverKind::River { .. }), _) => ( - 0, - g_water as u8 - + (if is_shaded { - 0.2 + shade_frac * 0.8 - } else { - alt - } * (127.0 - g_water)) as u8, - b_water as u8 - + (if is_shaded { - 0.2 + shade_frac * 0.8 - } else { - alt - } * (255.0 - b_water)) as u8, - 255, - ), - (None, _) | (Some(RiverKind::Lake { .. }), _) => ( - 0, - (((g_water - + if is_shaded { - 0.2 + shade_frac * 0.8 - } else { - 1.0 - } * water_alt - * (127.0 - g_water)) - + (-water_depth * g_water)) - * 1.0) as u8, - (((b_water - + if is_shaded { - 0.2 + shade_frac * 0.8 - } else { - 1.0 - } * water_alt - * (255.0 - b_water)) - + (-water_depth * b_water)) - * 1.0) as u8, - 255, - ), - }; - + 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 1762db8782..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, get_shadows, 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::*; @@ -1196,21 +1197,6 @@ impl WorldSim { None => false, }; - /* // Build a shadow map. - let shadows = get_shadows( - Vec3::new(-0.8, -1.0, 0.3), - TerrainChunkSize::RECT_SIZE.x, - TerrainChunkSize::RECT_SIZE.x as Alt, - TerrainChunkSize::RECT_SIZE.y as Alt, - Aabr { - min: Vec2::new(0.0, 0.0), - max: WORLD_SIZE.map(|e| e as Alt) * TerrainChunkSize::RECT_SIZE.map(|e| e as Alt), - }, - 0.0, - maxh, - |posi| alt[posi].max(water_alt[posi]), - ); */ - // Check whether any tiles around this tile are not water (since Lerp will // ensure that they are included). let pure_water = |posi: usize| { @@ -1322,22 +1308,20 @@ 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 shadow map. - let shadows = get_shadows( - Vec3::new( - map_config.light_direction.x, - map_config.light_direction.z, - map_config.light_direction.y, - ), + // 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, - TerrainChunkSize::RECT_SIZE.x as Alt, - TerrainChunkSize::RECT_SIZE.y as Alt, Aabr { - min: Vec2::new(0.0, 0.0), - max: WORLD_SIZE.map(|e| e as Alt) * TerrainChunkSize::RECT_SIZE.map(|e| e as Alt), + min: Vec2::zero(), + max: WORLD_SIZE.map(|e| e as i32), }, CONFIG.sea_level as Alt, (CONFIG.sea_level + self.max_height) as Alt, @@ -1345,6 +1329,8 @@ impl WorldSim { let chunk = &self.chunks[posi]; chunk.alt.max(chunk.water_alt) as Alt }, + |a| scale_angle(a), + |h| scale_height(h), ) .unwrap(); @@ -1363,18 +1349,34 @@ impl WorldSim { let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y]; // TODO: Parallelize again. - MapConfig { + let config = MapConfig { gain: self.max_height, - // lgain: 1.0, - shadows: Some(&shadows), samples: Some(&samples_data), - // is_sampled: true, + 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 11df60d835..3d7e6cfd63 100644 --- a/world/src/sim/util.rs +++ b/world/src/sim/util.rs @@ -1,5 +1,4 @@ use super::WORLD_SIZE; -use approx::ApproxEq; use bitvec::prelude::{bitbox, bitvec, BitBox}; use common::{terrain::TerrainChunkSize, vol::RectVolSize}; use noise::{MultiFractal, NoiseFn, Perlin, Point2, Point3, Point4, Seedable}; @@ -382,299 +381,76 @@ pub fn get_oceans(oldh: impl Fn(usize) -> F + Sync) -> BitBox { is_ocean } -/// Finds all shadowed chunks. -/// -/// ray should be a nonzero vector (if it's not, we return an error). -/// dx and dy should both be positive. -pub fn get_shadows( - ray: Vec3, +/// Finds the horizon map for sunlight for the given chunks. +pub fn get_horizon_map( lgain: F, - dx: F, - dy: F, - bounds: Aabr, + bounds: Aabr, minh: F, maxh: F, h: impl Fn(usize) -> F + Sync, -) -> Result, ()> { - // First, make sure the ray and delta aren't zero. - let ray = -ray; - let ray_squared = ray.magnitude_squared(); - if ray_squared == F::zero() - || ray_squared == F::neg_zero() - || !(dx > F::zero()) - || !(dy > F::zero()) - { - return Err(()); - } - let hsize = Vec2::new(dx, dy); - /* if hstep.is_approx_zero() { - return Err(()); - } */ - - // Find map sizes. - println!("Here?"); - let wmap_size = Vec2::::from(bounds.size()).map2(hsize, |e, f| e / f); - println!("Here?"); - let map_size = if let Vec2 { - x: Some(x), - y: Some(y), - } = wmap_size.map(|e| F::to_usize(&e)) - { - Vec2::new(x, y) - } else { - return Err(()); - }; + 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(); - /* let distance_map = |wposf: Vec3| { - // For all nodes in the height map, the minimum distance to a vertex is either - // the distance to the top of this chunk, or the distance to one of the neighbors. - let wpos = Vec2::new(i32::from(pos), i32::from(pos)); - let posi = vec2_as_uniform_idx(wpos); - let neighbor_min = neighbors(posi) - .map(|posj| Vec3::new(vec2_as_uniform_idx(wpos), h(posj)) - .min_by_key(|(pos, _)| pos.distance_squared(wpos)) - .unwrap_or(F::infinity()); - (wposf.z - ).min(neighobr_min) - } - - .min_by - wposf.z.min( - }; */ - - // Make sure that the ray has a horizontal component at all; if not, the ray is - // vertical, so it can't cast a shadow, and we set the brightness for each - // chunk to 1.0. - if Vec2::::from(ray).is_approx_zero() { - return Ok(vec![F::one(); map_len].into_boxed_slice()); - } - - // Conversely if the ray has no vertical component, each chunk must be entirely - // in shadow (at least for our purposes). - if ray.z == F::zero() || ray.z == F::neg_zero() { - return Ok(vec![F::zero(); map_len].into_boxed_slice()); - } - - // Otherwise, we can use step as the minimum length we need to raymarch in order - // to guarantee an accurate bounds check for the given dx and dy (i.e., all - // boundaries at spacings smaller than a "pixel" of size (dx,dy) should be - // taken into account). - let step_dot = if ray.x == F::zero() || ray.x == F::neg_zero() { - // Ray has no x component, so we must use y - dy * ray.y - } else if ray.y == F::zero() || ray.y == F::neg_zero() { - // Ray has no y component, so we must use x - dx * ray.x - } else { - // Ray has both an x and y component, so we must use the minimum. - if dy < dx { dy * ray.y } else { dx * ray.x } - } - .abs(); - let step = ray * (step_dot / ray_squared); - let hstep = Vec2::from(step); - let zstep = step.z; // Now, do the raymarching. - println!("Here?"); - let max_steps = if let Some(max_steps) = ((maxh - minh) / zstep) - .abs() - .min( - wmap_size.reduce_partial_max(), /* / hstep.reduce_partial_min() */ - ) - .to_usize() - { - max_steps + let chunk_x = if let Vec2 { x: Some(x), .. } = TerrainChunkSize::RECT_SIZE.map(F::from) { + x } else { return Err(()); }; - println!("Here?"); - let step_mag = step.magnitude(); - let two = F::one() + F::one(); - let three = two + F::one(); - let w = F::from(0.5).unwrap(); - let wstep_mag = lgain / (w * step_mag); - let wmax_steps = if let Some(wmax_steps) = F::from(max_steps) { - wmax_steps - } else { - return Err(()); - }; - println!("Here?"); - let chunk_size = if let Vec2 { - x: Some(x), - y: Some(y), - } = TerrainChunkSize::RECT_SIZE.map(F::from) - { - Vec2::new(x, y) - } else { - return Err(()); - }; - println!( - "Here? map_len={:?}, map_size={:?}, hstep={:?}, zstep={:?} max_steps={:?}", - map_len, map_size, hstep, zstep, max_steps - ); - Ok((0..map_len) - .into_par_iter() - .map(|posi| { - // Simple raymarch to determine whether we're in shadow. - // From https://www.iquilezles.org/www/articles/rmshadows/rmshadows.htm - // NOTE: How do we know these will succeed? - let wposf_orig = bounds.min - + Vec2::new( - F::from(posi % map_size.x).unwrap(), - F::from(posi / map_size.x).unwrap(), - ) * hsize; - let wpos_orig = wposf_orig.map2(chunk_size, |e, f| e / f); - // NOTE: How do we know these will succeed? - let wposl_orig = wpos_orig.map(|e| e.floor().to_i32().unwrap()); - // NOTE: How do we know these will succeed? - // let wposr_orig = wpos_orig.map(|e| e.ceil().to_i32().unwrap()); - - let h_orig = if wposl_orig.reduce_partial_min() < 0 || - // Casts are fine since we're a positive i32 - /*wposr_orig*/wposl_orig.x as usize >= WORLD_SIZE.x || - /*wposr_orig*/wposl_orig.y as usize >= WORLD_SIZE.y - { - // Out of bounds, assign minimum - minh - } else { - let hl_orig = h(vec2_as_uniform_idx(wposl_orig)); - // let hr_orig = h(vec2_as_uniform_idx(wposr_orig)); - // let wpos_frac = wposl_orig.map(|e| F::from(e).unwrap()).distance(wpos_orig); - hl_orig // hl_orig * wpos_frac + hr_orig * (F::one() - wpos_frac) - }; - - // let h_orig = h(vec2_as_uniform_idx(posi_orig.to_usize().unwrap())); - if h_orig < minh { - // Below the minimum height, always in shadow. - return F::zero(); - } - let wmax_steps = wmax_steps.min(((maxh - h_orig) / zstep).abs()); - // NOTE: How do we know these will succeed? - /* let wposf = bounds.min - + Vec2::new( - F::from(posi % map_size.x).unwrap(), - F::from(posi / map_size.x).unwrap(), - ) * hsize; */ - let mut s = F::one(); - // NOTE: How do we know these will succeed? - let mut wstep = F::zero(); - let mut h_i = h_orig; - let mut wposf_i = wposf_orig; - while wstep < wmax_steps && s >= F::zero() { - wstep = wstep + F::one(); - h_i = h_i + zstep; - wposf_i = wposf_i + hstep; - // Find height at this point. - // let h_i = h_orig + zstep * wstep; - // Find locations before and after h_i and use them to interpolate a height. - // let wposf_i = wposf + hstep * wstep; - let wpos_i = wposf_i.map2(chunk_size, |e, f| e / f); - // println!("h_orig={:?} h_i={:?}; wposf_orig={:?} wposf={:?}", h_orig, h_i, - // wposf, wposf_i); - // NOTE: How do we know these will succeed? - let wposl_i = wpos_i.map(|e| e.floor().to_i32().unwrap()); - // NOTE: How do we know these will succeed? - // let wposr_i = wpos_i.map(|e| e.ceil().to_i32().unwrap()); - if wposl_i.reduce_partial_min() < 0 || - // Casts are fine since we're a positive i32 - /*wposr_i*/wposl_i.x as usize >= WORLD_SIZE.x || - /*wposr_i*/wposl_i.y as usize >= WORLD_SIZE.y + // 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 { - // Out of bounds, we're done! - break; + return (to_angle(F::zero()), to_height(F::zero())); } - let hl_i = h(vec2_as_uniform_idx(wposl_i)); - // let hr_i = h(vec2_as_uniform_idx(wposr_i)); - // let wpos_frac = wposl_i.map(|e| F::from(e).unwrap()).distance(wpos_i); - let h_j = hl_i; //hl_i * wpos_frac + hr_i * (F::one() - wpos_frac); - // - /* let posj = wpos_i.map(|e| e.into_i32()); - let h_j = h(posj); */ - // If we landed in shadow, we're done. - // println!("h_orig={:?} h_i={:?} h_j={:?}; wposf_orig={:?} wposf={:?} - // wpos_frac={:?}", h_orig, h_i, h_j, wposf, wposf_i, wpos_frac); - // NOTE: Approximation to real distance metric, which is hard to compute for an - // arbitrary point in a heightmap. - // s = s.min((h_i - h_j) * lgain / (wstep * w * step_mag)); - s = s.min((h_i - h_j) * wstep_mag / wstep); - /* if s < F::zero() - /* h_i < h_j */ - { - // s = F::zero(); - // println!("A shadow!"); - break; - } */ - } - /*while h_i + zstep * posj < maxh { - wposf += hstep; - let posj : Vec2 = (wposf / chunk_size).into(); - // Simple interpolation. - let h_j = h(posj); - let h_j = h_i + (h_j - h_i) * (hstep / ).magnitude(); - h_i += zstep; - // If we landed in shadow, we're done. - if h_i > h_j { - return 0.0; - } - } */ - s = s.max(F::zero()); - // Smoothstep - s * s * (three - two * s) - // // Above the maximum height, definitely not in shadow. - // return F::one(); - /* let nh = h(posi); - if is_ocean(posi) { - -2 - } else { - let mut best = -1; - let mut besth = nh; - for nposi in neighbors(posi) { - let nbh = h(nposi); - if nbh < besth { - besth = nbh; - best = nposi as isize; + 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; + } } } - best - } */ - }) - .collect::>() - .into_boxed_slice()) - - /* // Raymarching technique to quickly identify approxmate distances - // Use a shadow map to raymarch all the height entries. - // 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 stack = Vec::new(); - let mut do_push = |pos| { - let posi = vec2_as_uniform_idx(pos); - if oldh(posi) <= F::zero() { - stack.push(posi); - } + let a = slope * lgain; + let h = h0 + max_height; + (to_angle(a), to_height(h)) + }) + .unzip_into_vecs(&mut angles, &mut heights); + (angles, heights) }; - for x in 0..WORLD_SIZE.x as i32 { - do_push(Vec2::new(x, 0)); - do_push(Vec2::new(x, WORLD_SIZE.y as i32 - 1)); - } - for y in 1..WORLD_SIZE.y as i32 - 1 { - do_push(Vec2::new(0, y)); - do_push(Vec2::new(WORLD_SIZE.x as i32 - 1, y)); - } - while let Some(chunk_idx) = stack.pop() { - // println!("Ocean chunk {:?}: {:?}", uniform_idx_as_vec2(chunk_idx), - // oldh(chunk_idx)); - if *is_ocean.at(chunk_idx) { - continue; - } - *is_ocean.at(chunk_idx) = true; - stack.extend(neighbors(chunk_idx).filter(|&neighbor_idx| { - // println!("Ocean neighbor: {:?}: {:?}", uniform_idx_as_vec2(neighbor_idx), - // oldh(neighbor_idx)); - oldh(neighbor_idx) <= F::zero() - })); - } - is_ocean */ + 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.