Horizon mapping and "layered" map generation.

Horizon mapping is a method of shadow mapping specific to height maps.
It can handle any angle between 0 and 90 degrees from the ground, as
long as know the horizontal direction in advance, by remembering only a
single angle (the "horizon angle" of the shadow map).  More is explained
in common/src/msg/server.rs.  We also remember the approximate height of
the largest occluder, to try to be able to generate soft shadows and
create a vertical position where the shadows can't go higher.

Additionally, map generation has been reworked.  Instead of computing
everything from explicit samples, we pass in sampling functions that
return exactly what the map generator needs.  This allows us to cleanly
separate the way we sample things like altitudes and colors from the map
generation process.  We exploit this to generate maps *partially* on the
server (with colors and rivers, but not shading).  We can then send the
partially completed map to the client, which can combine it with shadow
information to generate the final map.  This is useful for two reasons:
first, it makes sure the client can apply shadow information by itself,
and second, it lets us pass the unshaded map for use with level of
detail functionality.

For similar reasons, river generation is split
out into its own layer, but for now we opt to still generate rivers on
the server (since the river wire format is more complicated to compress
and may require some extra work to make sure we have enough precision to
draw rivers well enough for LoD).

Finally, the mostly ad-hoc lighting we were performing has been (mostly)
replaced with explicit Phong reflection shading (including specular
highlights).  Regularizing this seems useful and helps clarify the
"meaning" of the various light intensities, and helps us keep a more
physically plausible basis.  However, its interaction with soft shadows
is still imperfect, and it's not yet clear to me what we need to do to
turn this into something useful for LoD.
This commit is contained in:
Joshua Yanovski 2020-04-02 02:46:36 +02:00
parent f8926a5737
commit 32b2c99109
14 changed files with 925 additions and 651 deletions

3
Cargo.lock generated
View File

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

View File

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

View File

@ -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 / <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*/];
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.

View File

@ -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<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)]
pub enum ServerMsg {
InitialSync {
entity_package: sync::EntityPackage<EcsCompPacket>,
server_info: ServerInfo,
time_of_day: state::TimeOfDay,
world_map: (Vec2<u32>, Vec<u32>),
world_map: WorldMapMsg,
},
PlayerListUpdate(PlayerListUpdate),
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),

View File

@ -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<World>,
map: Vec<u32>,
map: WorldMapMsg,
postoffice: PostOffice<ServerMsg, ClientMsg>,
@ -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.");

View File

@ -5,7 +5,6 @@ authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
edition = "2018"
[dependencies]
approx = "0.1.1"
bincode = "1.2.0"
common = { package = "veloren-common", path = "../common" }
bitvec = "0.15.2"

View File

@ -30,6 +30,6 @@ fn main() {
}
}
win.update_with_buffer(&buf).unwrap();
win.update_with_buffer_size(&buf, W, H).unwrap();
}
}

View File

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

View File

@ -71,6 +71,6 @@ fn main() {
scale -= 6;
}
win.update_with_buffer(&buf).unwrap();
win.update_with_buffer_size(&buf, W, H).unwrap();
}
}

View File

@ -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::<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_shadows = |light_direction: Vec3<f64>, 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*/),
let refresh_horizons = |lgain, is_basement, is_water| {
get_horizon_map(
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() */,
min: Vec2::zero(),
max: WORLD_SIZE.map(|e| e as i32),
},
CONFIG.sea_level as f64, // focus.z,
(CONFIG.sea_level + sampler.max_height) as f64, // (focus.z + self.max_height) as Alt,
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 })
}
.max(if is_water {
sample.water_alt as f64
} else {
-f64::INFINITY
})
},
).ok()
|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)| {
} = 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)| {
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();
}
}

View File

@ -122,7 +122,7 @@ impl<'a> ColumnGen<'a> {
}
}
fn river_spline_coeffs(
pub fn river_spline_coeffs(
// _sim: &WorldSim,
chunk_pos: Vec2<f64>,
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
/// 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<Vec2<f64>>,
point: Vec2<f64>,
) -> Option<(f64, Vec2<f64>, f64)> {

View File

@ -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<f64>,
/// 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<Alt>, Vec<Alt>); 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,25 +116,101 @@ 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<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> {
/// 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.
pub fn generate(
&self,
sampler: &WorldSim,
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
) -> MapDebug {
/// 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 {
dimensions,
focus,
gain,
lgain,
scale,
light_direction,
shadows,
samples,
is_basement,
@ -139,31 +218,12 @@ impl<'a> MapConfig<'a> {
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 = 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 focus_rect = Vec2::from(focus);
let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64;
(0..dimensions.y * dimensions.x)
.into_iter()
.for_each(|chunk_idx| {
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 (
chunk_idx,
alt,
@ -173,6 +233,7 @@ impl<'a> MapConfig<'a> {
temperature,
downhill,
river_kind,
spline_derivative,
) = sampler
.get(pos)
.map(|sample| {
@ -185,6 +246,7 @@ impl<'a> MapConfig<'a> {
sample.temp,
sample.downhill,
sample.river.river_kind,
sample.river.spline_derivative,
)
})
.unwrap_or((
@ -196,10 +258,12 @@ impl<'a> MapConfig<'a> {
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 pos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let column_rgb = samples
.and_then(|samples| {
chunk_idx
@ -208,54 +272,306 @@ impl<'a> MapConfig<'a> {
.flatten()
})
.map(|sample| {
if is_basement {
sample.stone_col.map(|e| e as f64 / 255.0)
// 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 {
sample.surface_color.map(|e| e as f64)
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 column_rgb = if is_sampled {
column_sample.get(pos)
.map(|sample| if is_basement {
sample.stone_col.map(|e| e as f64 / 255.0)
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 {
sample.surface_color.map(|e| e as f64)
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
};*/
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)
},
}
}
/// 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,
sample_pos: impl Fn(Vec2<i32>) -> MapSample,
sample_wpos: impl Fn(Vec2<i32>) -> f32,
// sampler: &WorldSim,
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
) -> MapDebug {
let MapConfig {
dimensions,
focus,
gain,
lgain,
scale,
light_direction,
horizons,
is_shaded,
// is_debug,
..
} = *self;
let light_direction = Vec3::new(
light_direction.x,
light_direction.y,
0.0, // we currently ignore light_direction.z.
);
let light_shadow_dir = if light_direction.x >= 0.0 { 0 } else { 1 };
let horizon_map = horizons.map(|horizons| &horizons[light_shadow_dir]);
let light = light_direction.normalized();
let /*mut */quads = [[0u32; QUADRANTS]; QUADRANTS];
let /*mut */rivers = 0u32;
let /*mut */lakes = 0u32;
let /*mut */oceans = 0u32;
let focus_rect = Vec2::from(focus);
let chunk_size = TerrainChunkSize::RECT_SIZE.map(|e| e as f64);
(0..dimensions.y * dimensions.x)
.into_iter()
.for_each(|chunk_idx| {
let i = chunk_idx % dimensions.x as usize;
let j = chunk_idx / dimensions.x as usize;
let wposf = focus_rect + Vec2::new(i as f64, j as f64) * scale;
let pos = wposf.map(|e: f64| e as i32);
let wposf = wposf * chunk_size;
let chunk_idx = if pos.reduce_partial_min() >= 0
&& pos.x < WORLD_SIZE.x as i32
&& pos.y < WORLD_SIZE.y as i32
{
Some(vec2_as_uniform_idx(pos))
} else {
None
};
let MapSample {
rgb,
alt,
downhill_wpos,
..
} = sample_pos(pos);
let alt = alt as f32;
let wposi = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let mut rgb = rgb.map(|e| e as f64 / 255.0);
// Material properties:
//
// For each material in the scene,
// k_s = (RGB) specular reflection constant
let mut k_s = Rgb::new(1.0, 1.0, 1.0);
// k_d = (RGB) diffuse reflection constant
let mut k_d = rgb;
// k_a = (RGB) ambient reflection constant
let mut k_a = rgb;
// α = (per-material) shininess constant
let mut alpha = 4.0; // 4.0;
// Compute connections
let mut has_river = false;
// NOTE: consider replacing neighbors with local_cells, since it is more
// accurate (though I'm not sure if it can matter for these
// purposes).
chunk_idx
.map(|chunk_idx| neighbors(chunk_idx).chain(iter::once(chunk_idx)))
.into_iter()
.flatten()
.for_each(|neighbor_posi| {
let neighbor_pos = uniform_idx_as_vec2(neighbor_posi);
let neighbor_wpos = neighbor_pos.map(|e| e as f64) * chunk_size;
let MapSample { connections, .. } = sample_pos(neighbor_pos);
NEIGHBOR_DELTA
.iter()
.zip(
connections
.as_ref()
.map(|e| e.iter())
.into_iter()
.flatten()
.into_iter(),
)
.for_each(|(&delta, connection)| {
let connection = if let Some(connection) = connection {
connection
} else {
return;
};
let downhill_wpos = neighbor_wpos
+ Vec2::from(delta).map(|e: i32| e as f64) * chunk_size;
let coeffs = river_spline_coeffs(
neighbor_wpos,
connection.spline_derivative,
downhill_wpos,
);
let (_t, _pt, dist) = if let Some((t, pt, dist)) =
quadratic_nearest_point(&coeffs, wposf)
{
(t, pt, dist)
} else {
let ndist = wposf.distance_squared(neighbor_wpos);
let ddist = wposf.distance_squared(downhill_wpos);
if ndist <= ddist {
(0.0, neighbor_wpos, ndist)
} else {
(1.0, downhill_wpos, ddist)
}
};
let connection_dist = (dist.sqrt()
- (connection.width as f64 * 0.5).max(1.0))
.max(0.0);
if connection_dist == 0.0 {
match connection.kind {
ConnectionKind::River => {
has_river = true;
},
}
}
});
});
// Color in connectins.
let water_color_factor = 2.0;
let g_water = 32.0 * water_color_factor;
let b_water = 64.0 * water_color_factor;
if has_river {
let water_rgb = Rgb::new(0, ((g_water) * 1.0) as u8, ((b_water) * 1.0) as u8)
.map(|e| e as f64 / 255.0);
rgb = water_rgb;
k_s = Rgb::new(1.0, 1.0, 1.0);
k_d = water_rgb;
k_a = water_rgb;
alpha = 0.255;
}
let downhill_alt = sample_wpos(downhill_wpos);
let cross_pos = wposi
+ ((downhill_wpos - wposi)
.map(|e| e as f32)
.rotated_z(f32::consts::FRAC_PI_2)
.map(|e| e as i32));
let cross_alt = 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 = 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
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
};
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,
),
};
.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 rgba = (rgb.r, rgb.g, rgb.b, 255);
write_pixel(Vec2::new(i, j), rgba);
});

View File

@ -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<u32> {
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() * <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,
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
}
.generate(&self, |pos, (r, g, b, a)| {
};
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]);
});
v
},
);
WorldMapMsg {
dimensions: WORLD_SIZE.map(|e| e as u32),
max_height: self.max_height,
rgba: v,
horizons,
}
}
/// Prepare the world for simulation

View File

@ -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<F: Float>(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<F: std::iter::Sum + ApproxEq + Float + Send + Sync + std::fmt::Debug>(
ray: Vec3<F>,
/// Finds the horizon map for sunlight for the given chunks.
pub fn get_horizon_map<F: Float + Sync, A: Send, H: Send>(
lgain: F,
dx: F,
dy: F,
bounds: Aabr<F>,
bounds: Aabr<i32>,
minh: F,
maxh: F,
h: impl Fn(usize) -> F + Sync,
) -> Result<Box<[F]>, ()> {
// 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::<F>::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<A>, Vec<H>); 2], ()> {
let map_size = Vec2::<i32>::from(bounds.size()).map(|e| e as usize);
let map_len = map_size.product();
/* let distance_map = |wposf: Vec3<F>| {
// 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::<F>::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)
// 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| {
// 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
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, 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();
return (to_angle(F::zero()), to_height(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
{
// Out of bounds, we're done!
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 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<i32> = (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 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
} */
}
let a = slope * lgain;
let h = h0 + max_height;
(to_angle(a), to_height(h))
})
.collect::<Vec<_>>()
.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);
}
.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.