mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'sharp/map-colors' into sharp/small-fixes
This commit is contained in:
commit
88342640c6
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -4896,11 +4896,13 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
"image",
|
"image",
|
||||||
"log 0.4.8",
|
"log 0.4.8",
|
||||||
|
"num 0.2.1",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"specs",
|
"specs",
|
||||||
"uvth",
|
"uvth",
|
||||||
"vek 0.10.0",
|
"vek 0.10.0",
|
||||||
"veloren-common",
|
"veloren-common",
|
||||||
|
"veloren-world",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -6,10 +6,12 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { package = "veloren-common", path = "../common", features = ["no-assets"] }
|
common = { package = "veloren-common", path = "../common", features = ["no-assets"] }
|
||||||
|
world = { package = "veloren-world", path = "../world" }
|
||||||
|
|
||||||
byteorder = "1.3.2"
|
byteorder = "1.3.2"
|
||||||
uvth = "3.1.1"
|
uvth = "3.1.1"
|
||||||
image = "0.22.3"
|
image = "0.22.3"
|
||||||
|
num = "0.2.0"
|
||||||
num_cpus = "1.10.1"
|
num_cpus = "1.10.1"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
specs = "0.15.1"
|
specs = "0.15.1"
|
||||||
|
@ -30,9 +30,13 @@ use common::{
|
|||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
ChatType,
|
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 hashbrown::HashMap;
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
|
use num::traits::FloatConst;
|
||||||
use std::{
|
use std::{
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@ -40,6 +44,10 @@ use std::{
|
|||||||
};
|
};
|
||||||
use uvth::{ThreadPool, ThreadPoolBuilder};
|
use uvth::{ThreadPool, ThreadPoolBuilder};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
use world::{
|
||||||
|
sim::{neighbors, Alt},
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
// The duration of network inactivity until the player is kicked
|
// The duration of network inactivity until the player is kicked
|
||||||
// @TODO: in the future, this should be configurable on the server
|
// @TODO: in the future, this should be configurable on the server
|
||||||
@ -95,7 +103,7 @@ impl Client {
|
|||||||
entity_package,
|
entity_package,
|
||||||
server_info,
|
server_info,
|
||||||
time_of_day,
|
time_of_day,
|
||||||
world_map: (map_size, world_map),
|
world_map,
|
||||||
} => {
|
} => {
|
||||||
// TODO: Display that versions don't match in Voxygen
|
// TODO: Display that versions don't match in Voxygen
|
||||||
if server_info.git_hash != common::util::GIT_HASH.to_string() {
|
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);
|
let entity = state.ecs_mut().apply_entity_package(entity_package);
|
||||||
*state.ecs_mut().write_resource() = time_of_day;
|
*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 / <Alt as FloatConst>::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::<Vec<_>>(),
|
||||||
|
heights.into_iter().map(scale_height).collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
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<i32>| {
|
||||||
|
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*/];
|
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);
|
LittleEndian::write_u32_into(&world_map, &mut world_map_raw);
|
||||||
log::debug!("Preparing image...");
|
|
||||||
let world_map = Arc::new(
|
let world_map = Arc::new(
|
||||||
image::DynamicImage::ImageRgba8({
|
image::DynamicImage::ImageRgba8({
|
||||||
// Should not fail if the dimensions are correct.
|
// Should not fail if the dimensions are correct.
|
||||||
|
@ -25,13 +25,125 @@ pub enum PlayerListUpdate {
|
|||||||
Alias(u64, String),
|
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<u32>,
|
||||||
|
/// 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<u32>,
|
||||||
|
/// 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<u8>, Vec<u8>); 2],
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum ServerMsg {
|
pub enum ServerMsg {
|
||||||
InitialSync {
|
InitialSync {
|
||||||
entity_package: sync::EntityPackage<EcsCompPacket>,
|
entity_package: sync::EntityPackage<EcsCompPacket>,
|
||||||
server_info: ServerInfo,
|
server_info: ServerInfo,
|
||||||
time_of_day: state::TimeOfDay,
|
time_of_day: state::TimeOfDay,
|
||||||
world_map: (Vec2<u32>, Vec<u32>),
|
world_map: WorldMapMsg,
|
||||||
},
|
},
|
||||||
PlayerListUpdate(PlayerListUpdate),
|
PlayerListUpdate(PlayerListUpdate),
|
||||||
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
|
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
|
||||||
|
@ -28,7 +28,7 @@ use crate::{
|
|||||||
use common::{
|
use common::{
|
||||||
comp,
|
comp,
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
msg::{ClientMsg, ClientState, ServerInfo, ServerMsg},
|
msg::{server::WorldMapMsg, ClientMsg, ClientState, ServerInfo, ServerMsg},
|
||||||
net::PostOffice,
|
net::PostOffice,
|
||||||
state::{State, TimeOfDay},
|
state::{State, TimeOfDay},
|
||||||
sync::WorldSyncExt,
|
sync::WorldSyncExt,
|
||||||
@ -66,7 +66,7 @@ pub struct Tick(u64);
|
|||||||
pub struct Server {
|
pub struct Server {
|
||||||
state: State,
|
state: State,
|
||||||
world: Arc<World>,
|
world: Arc<World>,
|
||||||
map: Vec<u32>,
|
map: WorldMapMsg,
|
||||||
|
|
||||||
postoffice: PostOffice<ServerMsg, ClientMsg>,
|
postoffice: PostOffice<ServerMsg, ClientMsg>,
|
||||||
|
|
||||||
@ -117,7 +117,12 @@ impl Server {
|
|||||||
#[cfg(not(feature = "worldgen"))]
|
#[cfg(not(feature = "worldgen"))]
|
||||||
let world = World::generate(settings.world_seed);
|
let world = World::generate(settings.world_seed);
|
||||||
#[cfg(not(feature = "worldgen"))]
|
#[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")]
|
#[cfg(feature = "worldgen")]
|
||||||
let spawn_point = {
|
let spawn_point = {
|
||||||
@ -475,7 +480,7 @@ impl Server {
|
|||||||
.create_entity_package(entity, None, None, None),
|
.create_entity_package(entity, None, None, None),
|
||||||
server_info: self.server_info.clone(),
|
server_info: self.server_info.clone(),
|
||||||
time_of_day: *self.state.ecs().read_resource(),
|
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.");
|
log::debug!("Done initial sync with client.");
|
||||||
|
|
||||||
|
@ -30,6 +30,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
win.update_with_buffer(&buf).unwrap();
|
win.update_with_buffer_size(&buf, W, H).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
_time += 1.0 / 60.0;
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,6 @@ fn main() {
|
|||||||
scale -= 6;
|
scale -= 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
win.update_with_buffer(&buf).unwrap();
|
win.update_with_buffer_size(&buf, W, H).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
|
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
|
||||||
|
use rayon::prelude::*;
|
||||||
use std::{f64, io::Write, path::PathBuf, time::SystemTime};
|
use std::{f64, io::Write, path::PathBuf, time::SystemTime};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
use veloren_world::{
|
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,
|
World, CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,26 +23,92 @@ fn main() {
|
|||||||
let map_file =
|
let map_file =
|
||||||
// "map_1575990726223.bin";
|
// "map_1575990726223.bin";
|
||||||
// "map_1575987666972.bin";
|
// "map_1575987666972.bin";
|
||||||
"map_1576046079066.bin";
|
// "map_1576046079066.bin";
|
||||||
|
"map_1579539133272.bin";
|
||||||
let mut _map_file = PathBuf::from("./maps");
|
let mut _map_file = PathBuf::from("./maps");
|
||||||
_map_file.push(map_file);
|
_map_file.push(map_file);
|
||||||
|
|
||||||
let world = World::generate(5284, WorldOpts {
|
let world = World::generate(5284, WorldOpts {
|
||||||
seed_elements: false,
|
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::Load(_map_file),
|
||||||
world_file: sim::FileOpts::Save,
|
// world_file: sim::FileOpts::Save,
|
||||||
..WorldOpts::default()
|
..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::<Vec<_>>()
|
||||||
|
.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::<Vec<_>>()
|
||||||
|
.into_boxed_slice()
|
||||||
|
};
|
||||||
|
let get_map_sample = |map_samples: &[MapSample], pos: Vec2<i32>| {
|
||||||
|
if pos.reduce_partial_min() >= 0
|
||||||
|
&& pos.x < WORLD_SIZE.x as i32
|
||||||
|
&& pos.y < WORLD_SIZE.y as i32
|
||||||
|
{
|
||||||
|
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 =
|
let mut win =
|
||||||
minifb::Window::new("World Viewer", W, H, minifb::WindowOptions::default()).unwrap();
|
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);
|
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
|
// Altitude is divided by gain and clamped to [0, 1]; thus, decreasing gain
|
||||||
// makes smaller differences in altitude appear larger.
|
// 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,
|
// The Z component during normal calculations is multiplied by gain; thus,
|
||||||
let mut lgain = 1.0;
|
let mut lgain = 1.0;
|
||||||
let mut scale = WORLD_SIZE.x as f64 / W as f64;
|
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
|
// "In world space the x-axis will be pointing east, the y-axis up and the
|
||||||
// z-axis will be pointing south"
|
// 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_basement = false;
|
||||||
let mut is_water = true;
|
let mut is_water = true;
|
||||||
@ -58,6 +129,11 @@ fn main() {
|
|||||||
let mut is_temperature = true;
|
let mut is_temperature = true;
|
||||||
let mut is_humidity = 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() {
|
while win.is_open() {
|
||||||
let config = MapConfig {
|
let config = MapConfig {
|
||||||
dimensions: Vec2::new(W, H),
|
dimensions: Vec2::new(W, H),
|
||||||
@ -66,6 +142,8 @@ fn main() {
|
|||||||
lgain,
|
lgain,
|
||||||
scale,
|
scale,
|
||||||
light_direction,
|
light_direction,
|
||||||
|
horizons: horizons.as_ref(), /* .map(|(a, b)| (&**a, &**b)) */
|
||||||
|
samples,
|
||||||
|
|
||||||
is_basement,
|
is_basement,
|
||||||
is_water,
|
is_water,
|
||||||
@ -75,19 +153,30 @@ fn main() {
|
|||||||
is_debug: true,
|
is_debug: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if samples_changed {
|
||||||
|
map_samples = refresh_map_samples(&config);
|
||||||
|
};
|
||||||
|
|
||||||
let mut buf = vec![0; W * H];
|
let mut buf = vec![0; W * H];
|
||||||
let MapDebug {
|
let MapDebug {
|
||||||
rivers,
|
rivers,
|
||||||
lakes,
|
lakes,
|
||||||
oceans,
|
oceans,
|
||||||
quads,
|
quads,
|
||||||
} = config.generate(sampler, |pos, (r, g, b, a)| {
|
} = config.generate(
|
||||||
let i = pos.x;
|
|pos| get_map_sample(&map_samples, pos),
|
||||||
let j = pos.y;
|
|pos| config.sample_wpos(sampler, pos),
|
||||||
buf[j * W + i] = u32::from_le_bytes([b, g, r, a]);
|
|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) {
|
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)
|
if let Some(len) = (W * H)
|
||||||
.checked_mul(scale as usize)
|
.checked_mul(scale as usize)
|
||||||
.and_then(|acc| acc.checked_mul(scale as usize))
|
.and_then(|acc| acc.checked_mul(scale as usize))
|
||||||
@ -100,11 +189,15 @@ fn main() {
|
|||||||
..config
|
..config
|
||||||
};
|
};
|
||||||
let mut buf = vec![0u8; 4 * len];
|
let mut buf = vec![0u8; 4 * len];
|
||||||
config.generate(sampler, |pos, (r, g, b, a)| {
|
config.generate(
|
||||||
let i = pos.x;
|
|pos| get_map_sample(&map_samples, pos),
|
||||||
let j = pos.y;
|
|pos| config.sample_wpos(sampler, pos),
|
||||||
(&mut buf[(j * x + i) * 4..]).write(&[r, g, b, a]).unwrap();
|
|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.
|
// TODO: Justify fits in u32.
|
||||||
let world_map = image::RgbaImage::from_raw(x as u32, y as u32, buf)
|
let world_map = image::RgbaImage::from_raw(x as u32, y as u32, buf)
|
||||||
.expect("Image dimensions must be valid");
|
.expect("Image dimensions must be valid");
|
||||||
@ -161,18 +254,39 @@ fn main() {
|
|||||||
let is_camera = win.is_key_down(minifb::Key::C);
|
let is_camera = win.is_key_down(minifb::Key::C);
|
||||||
if win.is_key_down(minifb::Key::B) {
|
if win.is_key_down(minifb::Key::B) {
|
||||||
is_basement ^= true;
|
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) {
|
if win.is_key_down(minifb::Key::H) {
|
||||||
is_humidity ^= true;
|
is_humidity ^= true;
|
||||||
|
samples_changed = true;
|
||||||
}
|
}
|
||||||
if win.is_key_down(minifb::Key::T) {
|
if win.is_key_down(minifb::Key::T) {
|
||||||
is_temperature ^= true;
|
is_temperature ^= true;
|
||||||
|
samples_changed = true;
|
||||||
}
|
}
|
||||||
if win.is_key_down(minifb::Key::O) {
|
if win.is_key_down(minifb::Key::O) {
|
||||||
is_water ^= true;
|
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 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 win.is_key_down(minifb::Key::W) {
|
||||||
if is_camera {
|
if is_camera {
|
||||||
@ -206,6 +320,8 @@ fn main() {
|
|||||||
if is_camera {
|
if is_camera {
|
||||||
if (lgain * 2.0).is_normal() {
|
if (lgain * 2.0).is_normal() {
|
||||||
lgain *= 2.0;
|
lgain *= 2.0;
|
||||||
|
horizons =
|
||||||
|
horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gain += 64.0;
|
gain += 64.0;
|
||||||
@ -215,6 +331,8 @@ fn main() {
|
|||||||
if is_camera {
|
if is_camera {
|
||||||
if (lgain / 2.0).is_normal() {
|
if (lgain / 2.0).is_normal() {
|
||||||
lgain /= 2.0;
|
lgain /= 2.0;
|
||||||
|
horizons =
|
||||||
|
horizons.and_then(|_| refresh_horizons(lgain, is_basement, is_water));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gain = (gain - 64.0).max(64.0);
|
gain = (gain - 64.0).max(64.0);
|
||||||
@ -223,6 +341,7 @@ fn main() {
|
|||||||
if win.is_key_down(minifb::Key::R) {
|
if win.is_key_down(minifb::Key::R) {
|
||||||
if is_camera {
|
if is_camera {
|
||||||
focus.z += spd * scale;
|
focus.z += spd * scale;
|
||||||
|
samples_changed = true;
|
||||||
} else {
|
} else {
|
||||||
if (scale * 2.0).is_normal() {
|
if (scale * 2.0).is_normal() {
|
||||||
scale *= 2.0;
|
scale *= 2.0;
|
||||||
@ -232,6 +351,7 @@ fn main() {
|
|||||||
if win.is_key_down(minifb::Key::F) {
|
if win.is_key_down(minifb::Key::F) {
|
||||||
if is_camera {
|
if is_camera {
|
||||||
focus.z -= spd * scale;
|
focus.z -= spd * scale;
|
||||||
|
samples_changed = true;
|
||||||
} else {
|
} else {
|
||||||
if (scale / 2.0).is_normal() {
|
if (scale / 2.0).is_normal() {
|
||||||
scale /= 2.0;
|
scale /= 2.0;
|
||||||
@ -239,6 +359,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
win.update_with_buffer(&buf).unwrap();
|
win.update_with_buffer_size(&buf, W, H).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ impl<'a> ColumnGen<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn river_spline_coeffs(
|
pub fn river_spline_coeffs(
|
||||||
// _sim: &WorldSim,
|
// _sim: &WorldSim,
|
||||||
chunk_pos: Vec2<f64>,
|
chunk_pos: Vec2<f64>,
|
||||||
spline_derivative: Vec2<f32>,
|
spline_derivative: Vec2<f32>,
|
||||||
@ -145,7 +145,7 @@ fn river_spline_coeffs(
|
|||||||
/// curve"... hopefully this works out okay and gives us what we want (a
|
/// curve"... hopefully this works out okay and gives us what we want (a
|
||||||
/// river that extends outwards tangent to a quadratic curve, with width
|
/// river that extends outwards tangent to a quadratic curve, with width
|
||||||
/// configured by distance along the line).
|
/// configured by distance along the line).
|
||||||
fn quadratic_nearest_point(
|
pub fn quadratic_nearest_point(
|
||||||
spline: &Vec3<Vec2<f64>>,
|
spline: &Vec3<Vec2<f64>>,
|
||||||
point: Vec2<f64>,
|
point: Vec2<f64>,
|
||||||
) -> Option<(f64, Vec2<f64>, f64)> {
|
) -> Option<(f64, Vec2<f64>, f64)> {
|
||||||
|
@ -64,5 +64,5 @@ pub const CONFIG: Config = Config {
|
|||||||
river_roughness: 0.06125,
|
river_roughness: 0.06125,
|
||||||
river_max_width: 2.0,
|
river_max_width: 2.0,
|
||||||
river_min_height: 0.25,
|
river_min_height: 0.25,
|
||||||
river_width_to_depth: 1.0,
|
river_width_to_depth: 8.0,
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
use crate::{
|
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,
|
CONFIG,
|
||||||
};
|
};
|
||||||
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
|
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
|
||||||
use std::{f32, f64};
|
use std::{f32, f64, iter};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
pub struct MapConfig {
|
pub struct MapConfig<'a> {
|
||||||
/// Dimensions of the window being written to. Defaults to WORLD_SIZE.
|
/// Dimensions of the window being written to. Defaults to WORLD_SIZE.
|
||||||
pub dimensions: Vec2<usize>,
|
pub dimensions: Vec2<usize>,
|
||||||
/// x, y, and z of top left of map (defaults to (0.0, 0.0,
|
/// 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).
|
/// Defaults to (-0.8, -1.0, 0.3).
|
||||||
pub light_direction: Vec3<f64>,
|
pub light_direction: Vec3<f64>,
|
||||||
|
/// If Some, uses the provided horizon map.
|
||||||
|
///
|
||||||
|
/// Defaults to None.
|
||||||
|
pub horizons: Option<&'a [(Vec<Alt>, Vec<Alt>); 2]>,
|
||||||
|
/// If Some, uses the provided column samples to determine surface color.
|
||||||
|
///
|
||||||
|
/// Defaults to None.
|
||||||
|
pub samples: Option<&'a [Option<ColumnSample<'a>>]>,
|
||||||
/// If true, only the basement (bedrock) is used for altitude; otherwise,
|
/// If true, only the basement (bedrock) is used for altitude; otherwise,
|
||||||
/// the surface is used.
|
/// the surface is used.
|
||||||
///
|
///
|
||||||
@ -81,7 +93,7 @@ pub struct MapDebug {
|
|||||||
pub oceans: u32,
|
pub oceans: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MapConfig {
|
impl<'a> Default for MapConfig<'a> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let dimensions = WORLD_SIZE;
|
let dimensions = WORLD_SIZE;
|
||||||
Self {
|
Self {
|
||||||
@ -90,7 +102,9 @@ impl Default for MapConfig {
|
|||||||
gain: CONFIG.mountain_scale,
|
gain: CONFIG.mountain_scale,
|
||||||
lgain: TerrainChunkSize::RECT_SIZE.x as f64,
|
lgain: TerrainChunkSize::RECT_SIZE.x as f64,
|
||||||
scale: WORLD_SIZE.x as f64 / dimensions.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_basement: false,
|
||||||
is_water: true,
|
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<f32>,
|
||||||
|
/// Width of the connection.
|
||||||
|
pub width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-chunk data the map needs to be able to sample in order to correctly
|
||||||
|
/// render.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MapSample {
|
||||||
|
/// the base RGB color for a particular map pixel using the current settings
|
||||||
|
/// (i.e. the color *without* lighting).
|
||||||
|
pub rgb: Rgb<u8>,
|
||||||
|
/// Surface altitude information
|
||||||
|
/// (correctly reflecting settings like is_basement and is_water)
|
||||||
|
pub alt: f64,
|
||||||
|
/// Downhill chunk (may not be meaningful on ocean tiles, or at least edge
|
||||||
|
/// tiles)
|
||||||
|
pub downhill_wpos: Vec2<i32>,
|
||||||
|
/// Connection information about any connections to/from this chunk (e.g.
|
||||||
|
/// rivers).
|
||||||
|
///
|
||||||
|
/// Connections at each index correspond to the same index in
|
||||||
|
/// NEIGHBOR_DELTA.
|
||||||
|
pub connections: Option<[Option<Connection>; 8]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MapConfig<'a> {
|
||||||
|
/// A sample function that grabs the connections at a chunk.
|
||||||
|
///
|
||||||
|
/// Currently this just supports rivers, but ideally it can be extended past
|
||||||
|
/// that.
|
||||||
|
///
|
||||||
|
/// A sample function that grabs surface altitude at a column.
|
||||||
|
/// (correctly reflecting settings like is_basement and is_water).
|
||||||
|
///
|
||||||
|
/// The altitude produced by this function at a column corresponding to a
|
||||||
|
/// particular chunk should be identical to the altitude produced by
|
||||||
|
/// sample_pos at that chunk.
|
||||||
|
///
|
||||||
|
/// You should generally pass a closure over this function into generate
|
||||||
|
/// when constructing a map for the first time.
|
||||||
|
/// However, if repeated construction is needed, or alternate base colors
|
||||||
|
/// are to be used for some reason, one should pass a custom function to
|
||||||
|
/// generate instead (e.g. one that just looks up the height in a cached
|
||||||
|
/// array).
|
||||||
|
pub fn sample_wpos(&self, sampler: &WorldSim, wpos: Vec2<i32>) -> f32 {
|
||||||
|
let MapConfig {
|
||||||
|
focus,
|
||||||
|
gain,
|
||||||
|
|
||||||
|
is_basement,
|
||||||
|
is_water,
|
||||||
|
..
|
||||||
|
} = *self;
|
||||||
|
|
||||||
|
(sampler
|
||||||
|
.get_wpos(wpos)
|
||||||
|
.map(|s| {
|
||||||
|
if is_basement { s.basement } else { s.alt }.max(if is_water {
|
||||||
|
s.water_alt
|
||||||
|
} else {
|
||||||
|
-f32::INFINITY
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(CONFIG.sea_level)
|
||||||
|
- focus.z as f32)
|
||||||
|
/ gain as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Samples a MapSample at a chunk.
|
||||||
|
///
|
||||||
|
/// You should generally pass a closure over this function into generate
|
||||||
|
/// when constructing a map for the first time.
|
||||||
|
/// However, if repeated construction is needed, or alternate base colors
|
||||||
|
/// are to be used for some reason, one should pass a custom function to
|
||||||
|
/// generate instead (e.g. one that just looks up the color in a cached
|
||||||
|
/// array).
|
||||||
|
pub fn sample_pos(&self, sampler: &WorldSim, pos: Vec2<i32>) -> MapSample {
|
||||||
|
let MapConfig {
|
||||||
|
focus,
|
||||||
|
gain,
|
||||||
|
samples,
|
||||||
|
|
||||||
|
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
|
/// Generates a map image using the specified settings. Note that it will
|
||||||
/// write from left to write from (0, 0) to dimensions - 1, inclusive,
|
/// 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
|
/// 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
|
/// to the caller to provide a function that translates this information
|
||||||
/// into the correct format for a buffer and writes to it.
|
/// 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(
|
pub fn generate(
|
||||||
&self,
|
&self,
|
||||||
sampler: &WorldSim,
|
sample_pos: impl Fn(Vec2<i32>) -> MapSample,
|
||||||
|
sample_wpos: impl Fn(Vec2<i32>) -> f32,
|
||||||
|
// sampler: &WorldSim,
|
||||||
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
|
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
|
||||||
) -> MapDebug {
|
) -> MapDebug {
|
||||||
let MapConfig {
|
let MapConfig {
|
||||||
@ -120,23 +410,29 @@ impl MapConfig {
|
|||||||
lgain,
|
lgain,
|
||||||
scale,
|
scale,
|
||||||
light_direction,
|
light_direction,
|
||||||
|
horizons,
|
||||||
|
|
||||||
is_basement,
|
|
||||||
is_water,
|
|
||||||
is_shaded,
|
is_shaded,
|
||||||
is_temperature,
|
// is_debug,
|
||||||
is_humidity,
|
..
|
||||||
is_debug,
|
|
||||||
} = *self;
|
} = *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 light = light_direction.normalized();
|
||||||
let mut quads = [[0u32; QUADRANTS]; QUADRANTS];
|
let /*mut */quads = [[0u32; QUADRANTS]; QUADRANTS];
|
||||||
let mut rivers = 0u32;
|
let /*mut */rivers = 0u32;
|
||||||
let mut lakes = 0u32;
|
let /*mut */lakes = 0u32;
|
||||||
let mut oceans = 0u32;
|
let /*mut */oceans = 0u32;
|
||||||
|
|
||||||
let focus_rect = Vec2::from(focus);
|
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)
|
(0..dimensions.y * dimensions.x)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -144,81 +440,148 @@ impl MapConfig {
|
|||||||
let i = chunk_idx % dimensions.x as usize;
|
let i = chunk_idx % dimensions.x as usize;
|
||||||
let j = chunk_idx / dimensions.x as usize;
|
let j = chunk_idx / dimensions.x as usize;
|
||||||
|
|
||||||
let pos =
|
let wposf = focus_rect + Vec2::new(i as f64, j as f64) * scale;
|
||||||
(focus_rect + Vec2::new(i as f64, j as f64) * scale).map(|e: f64| e as i32);
|
let pos = wposf.map(|e: f64| e as i32);
|
||||||
|
let wposf = wposf * chunk_size;
|
||||||
|
|
||||||
let (alt, basement, water_alt, humidity, temperature, downhill, river_kind) =
|
let chunk_idx = if pos.reduce_partial_min() >= 0
|
||||||
sampler
|
&& pos.x < WORLD_SIZE.x as i32
|
||||||
.get(pos)
|
&& pos.y < WORLD_SIZE.y as i32
|
||||||
.map(|sample| {
|
{
|
||||||
(
|
Some(vec2_as_uniform_idx(pos))
|
||||||
sample.alt,
|
} else {
|
||||||
sample.basement,
|
None
|
||||||
sample.water_alt,
|
};
|
||||||
sample.humidity,
|
|
||||||
sample.temp,
|
let MapSample {
|
||||||
sample.downhill,
|
rgb,
|
||||||
sample.river.river_kind,
|
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)| {
|
||||||
.unwrap_or((
|
let connection = if let Some(connection) = connection {
|
||||||
CONFIG.sea_level,
|
connection
|
||||||
CONFIG.sea_level,
|
} else {
|
||||||
CONFIG.sea_level,
|
return;
|
||||||
0.0,
|
};
|
||||||
0.0,
|
let downhill_wpos = neighbor_wpos
|
||||||
None,
|
+ Vec2::from(delta).map(|e: i32| e as f64) * chunk_size;
|
||||||
None,
|
let coeffs = river_spline_coeffs(
|
||||||
));
|
neighbor_wpos,
|
||||||
let humidity = humidity.min(1.0).max(0.0);
|
connection.spline_derivative,
|
||||||
let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5;
|
downhill_wpos,
|
||||||
let pos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
|
);
|
||||||
let downhill_pos = (downhill
|
let (_t, _pt, dist) = if let Some((t, pt, dist)) =
|
||||||
.map(|downhill_pos| downhill_pos)
|
quadratic_nearest_point(&coeffs, wposf)
|
||||||
.unwrap_or(pos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32))
|
{
|
||||||
- pos)
|
(t, pt, dist)
|
||||||
+ pos;
|
} else {
|
||||||
let downhill_alt = sampler
|
let ndist = wposf.distance_squared(neighbor_wpos);
|
||||||
.get_wpos(downhill_pos)
|
let ddist = wposf.distance_squared(downhill_wpos);
|
||||||
.map(|s| if is_basement { s.basement } else { s.alt })
|
if ndist <= ddist {
|
||||||
.unwrap_or(CONFIG.sea_level);
|
(0.0, neighbor_wpos, ndist)
|
||||||
let alt = if is_basement { basement } else { alt };
|
} else {
|
||||||
let cross_pos = pos
|
(1.0, downhill_wpos, ddist)
|
||||||
+ ((downhill_pos - pos)
|
}
|
||||||
|
};
|
||||||
|
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)
|
.map(|e| e as f32)
|
||||||
.rotated_z(f32::consts::FRAC_PI_2)
|
.rotated_z(f32::consts::FRAC_PI_2)
|
||||||
.map(|e| e as i32));
|
.map(|e| e as i32));
|
||||||
let cross_alt = sampler
|
let cross_alt = sample_wpos(cross_pos);
|
||||||
.get_wpos(cross_pos)
|
|
||||||
.map(|s| if is_basement { s.basement } else { s.alt })
|
|
||||||
.unwrap_or(CONFIG.sea_level);
|
|
||||||
// Pointing downhill, forward
|
// Pointing downhill, forward
|
||||||
// (index--note that (0,0,1) is backward right-handed)
|
// (index--note that (0,0,1) is backward right-handed)
|
||||||
let forward_vec = Vec3::new(
|
let forward_vec = Vec3::new(
|
||||||
(downhill_pos.x - pos.x) as f64,
|
(downhill_wpos.x - wposi.x) as f64,
|
||||||
(downhill_alt - alt) as f64 * lgain,
|
((downhill_alt - alt) * gain) as f64 * lgain,
|
||||||
(downhill_pos.y - pos.y) as f64,
|
(downhill_wpos.y - wposi.y) as f64,
|
||||||
);
|
);
|
||||||
// Pointing 90 degrees left (in horizontal xy) of downhill, up
|
// Pointing 90 degrees left (in horizontal xy) of downhill, up
|
||||||
// (middle--note that (1,0,0), 90 degrees CCW backward, is right right-handed)
|
// (middle--note that (1,0,0), 90 degrees CCW backward, is right right-handed)
|
||||||
let up_vec = Vec3::new(
|
let up_vec = Vec3::new(
|
||||||
(cross_pos.x - pos.x) as f64,
|
(cross_pos.x - wposi.x) as f64,
|
||||||
(cross_alt - alt) as f64 * lgain,
|
((cross_alt - alt) * gain) as f64 * lgain,
|
||||||
(cross_pos.y - pos.y) as f64,
|
(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
|
// Then cross points "to the right" (upwards) on a right-handed coordinate
|
||||||
// system. (right-handed coordinate system means (0, 0, 1.0) is
|
// system. (right-handed coordinate system means (0, 0, 1.0) is
|
||||||
// "forward" into the screen).
|
// "forward" into the screen).
|
||||||
let surface_normal = forward_vec.cross(up_vec).normalized();
|
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;
|
// TODO: Figure out if we can reimplement debugging.
|
||||||
let true_alt = (alt as f64 - focus.z) / gain as f64;
|
/* if is_debug {
|
||||||
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 {
|
|
||||||
let quad = |x: f32| {
|
let quad = |x: f32| {
|
||||||
((x as f64 * QUADRANTS as f64).floor() as usize).min(QUADRANTS - 1)
|
((x as f64 * QUADRANTS as f64).floor() as usize).min(QUADRANTS - 1)
|
||||||
};
|
};
|
||||||
@ -237,62 +600,84 @@ impl MapConfig {
|
|||||||
},
|
},
|
||||||
None => {},
|
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 rgba = (rgb.r, rgb.g, rgb.b, 255);
|
||||||
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);
|
|
||||||
|
|
||||||
write_pixel(Vec2::new(i, j), rgba);
|
write_pixel(Vec2::new(i, j), rgba);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,12 +14,12 @@ pub use self::{
|
|||||||
get_rivers, mrec_downhill, Alt, RiverData, RiverKind,
|
get_rivers, mrec_downhill, Alt, RiverData, RiverKind,
|
||||||
},
|
},
|
||||||
location::Location,
|
location::Location,
|
||||||
map::{MapConfig, MapDebug},
|
map::{MapConfig, MapDebug, MapSample},
|
||||||
settlement::Settlement,
|
settlement::Settlement,
|
||||||
util::{
|
util::{
|
||||||
cdf_irwin_hall, downhill, get_oceans, local_cells, map_edge_factor, neighbors,
|
cdf_irwin_hall, downhill, get_horizon_map, get_oceans, local_cells, map_edge_factor,
|
||||||
uniform_idx_as_vec2, uniform_noise, uphill, vec2_as_uniform_idx, InverseCdf, ScaleBias,
|
neighbors, uniform_idx_as_vec2, uniform_noise, uphill, vec2_as_uniform_idx, InverseCdf,
|
||||||
NEIGHBOR_DELTA,
|
ScaleBias, NEIGHBOR_DELTA,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
assets,
|
assets,
|
||||||
|
msg::server::WorldMapMsg,
|
||||||
terrain::{BiomeKind, TerrainChunkSize},
|
terrain::{BiomeKind, TerrainChunkSize},
|
||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
@ -41,7 +42,7 @@ use noise::{
|
|||||||
BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti,
|
BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti,
|
||||||
Seedable, SuperSimplex, Worley,
|
Seedable, SuperSimplex, Worley,
|
||||||
};
|
};
|
||||||
use num::{Float, Signed};
|
use num::{traits::FloatConst, Float, Signed};
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
use rand_chacha::ChaChaRng;
|
use rand_chacha::ChaChaRng;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
@ -1101,9 +1102,9 @@ impl WorldSim {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
let flux_old = get_multi_drainage(&mstack, &mrec, &*mwrec, boundary_len);
|
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.
|
// 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 water_height_initial = |chunk_idx| {
|
||||||
let indirection_idx = indirection[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
|
/// Draw a map of the world based on chunk information. Returns a buffer of
|
||||||
/// u32s.
|
/// u32s.
|
||||||
pub fn get_map(&self) -> Vec<u32> {
|
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() * <Alt as FloatConst>::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::<Vec<_>>()
|
||||||
|
.into_boxed_slice()
|
||||||
|
};
|
||||||
|
|
||||||
let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y];
|
let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y];
|
||||||
// TODO: Parallelize again.
|
// TODO: Parallelize again.
|
||||||
MapConfig {
|
let config = MapConfig {
|
||||||
gain: self.max_height,
|
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
|
/// Prepare the world for simulation
|
||||||
|
@ -294,6 +294,52 @@ pub fn downhill<F: Float>(
|
|||||||
.into_boxed_slice()
|
.into_boxed_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* /// Bilinear interpolation.
|
||||||
|
///
|
||||||
|
/// Linear interpolation in both directions (i.e. quadratic interpolation).
|
||||||
|
fn get_interpolated_bilinear<T, F>(&self, pos: Vec2<i32>, mut f: F) -> Option<T>
|
||||||
|
where
|
||||||
|
T: Copy + Default + Signed + Float + Add<Output = T> + Mul<f32, Output = T>,
|
||||||
|
F: FnMut(Vec2<i32>) -> Option<T>,
|
||||||
|
{
|
||||||
|
// (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
|
/// Find all ocean tiles from a height map, using an inductive definition of
|
||||||
/// ocean as one of:
|
/// ocean as one of:
|
||||||
/// - posi is at the side of the world (map_edge_factor(posi) == 0.0)
|
/// - posi is at the side of the world (map_edge_factor(posi) == 0.0)
|
||||||
@ -335,6 +381,78 @@ pub fn get_oceans<F: Float>(oldh: impl Fn(usize) -> F + Sync) -> BitBox {
|
|||||||
is_ocean
|
is_ocean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the horizon map for sunlight for the given chunks.
|
||||||
|
pub fn get_horizon_map<F: Float + Sync, A: Send, H: Send>(
|
||||||
|
lgain: F,
|
||||||
|
bounds: Aabr<i32>,
|
||||||
|
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<A>, Vec<H>); 2], ()> {
|
||||||
|
let map_size = Vec2::<i32>::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.
|
/// A 2-dimensional vector, for internal use.
|
||||||
type Vector2<T> = [T; 2];
|
type Vector2<T> = [T; 2];
|
||||||
/// A 3-dimensional vector, for internal use.
|
/// A 3-dimensional vector, for internal use.
|
||||||
|
Loading…
Reference in New Issue
Block a user