Send client 3D rendered map.

Also shares configurable rendering between map generator and server.
This commit is contained in:
Joshua Yanovski 2020-01-13 08:10:38 +01:00
parent 9ee0cd82d0
commit ebe0d14eab
5 changed files with 360 additions and 233 deletions

1
Cargo.lock generated
View File

@ -1572,6 +1572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ahash 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
"autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@ -76,6 +76,7 @@ pub struct Tick(u64);
pub struct Server {
state: State,
world: Arc<World>,
map: Vec<u32>,
postoffice: PostOffice<ServerMsg, ClientMsg>,
@ -119,6 +120,7 @@ impl Server {
..WorldOpts::default()
},
);
let map = world.sim().get_map();
let spawn_point = {
// NOTE: all of these `.map(|e| e as [type])` calls should compile into no-ops,
@ -178,6 +180,7 @@ impl Server {
let this = Self {
state,
world: Arc::new(world),
map,
postoffice: PostOffice::bind(settings.gameserver_address)?,
@ -1084,7 +1087,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.world.sim().get_map()),
world_map: (WORLD_SIZE.map(|e| e as u32), self.map.clone()),
});
log::debug!("Done initial sync with client.");

View File

@ -3,7 +3,7 @@ use common::{terrain::TerrainChunkSize, vol::RectVolSize};
use std::{f32, f64, path::PathBuf};
use vek::*;
use veloren_world::{
sim::{self, RiverKind, WorldOpts, WORLD_SIZE},
sim::{self, MapConfig, MapDebug, RiverKind, WorldOpts, WORLD_SIZE},
util::Sampler,
World, CONFIG,
};
@ -46,11 +46,11 @@ fn main() {
},
);
let sampler = world.sim();
let mut win =
minifb::Window::new("World Viewer", W, H, minifb::WindowOptions::default()).unwrap();
let sampler = world.sim();
let mut focus = Vec3::new(0.0, 0.0, CONFIG.sea_level as f64);
// Altitude is divided by gain and clamped to [0, 1]; thus, decreasing gain makes
// smaller differences in altitude appear larger.
@ -67,7 +67,6 @@ fn main() {
//
// "In world space the x-axis will be pointing east, the y-axis up and the z-axis will be pointing south"
let mut light_direction = Vec3::new(-0.8, -1.0, 0.3);
let light_res = 3;
let mut is_basement = false;
let mut is_water = true;
@ -76,199 +75,33 @@ fn main() {
let mut is_humidity = true;
while win.is_open() {
let light = light_direction.normalized();
let config = MapConfig {
dimensions: Vec2::new(W, H),
focus,
gain,
lgain,
scale,
light_direction,
is_basement,
is_water,
is_shaded,
is_temperature,
is_humidity,
is_debug: true,
};
let mut buf = vec![0; W * H];
const QUADRANTS: usize = 4;
let mut quads = [[0u32; QUADRANTS]; QUADRANTS];
let mut rivers = 0u32;
let mut lakes = 0u32;
let mut oceans = 0u32;
// let water_light = (light_direction.z + 1.0) / 2.0 * 0.8 + 0.2;
let focus_rect = Vec2::from(focus);
let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64;
for i in 0..W {
for j in 0..H {
let pos =
(focus_rect + Vec2::new(i as f64, j as f64) * scale).map(|e: f64| e as i32);
/* let top_left = pos;
let top_right = focus + Vec2::new(i as i32 + light_res, j as i32) * scale;
let bottom_left = focus + Vec2::new(i as i32, j as i32 + light_res) * scale; */
let (alt, basement, water_alt, humidity, temperature, downhill, river_kind) =
sampler
.get(pos)
.map(|sample| {
(
sample.alt,
sample.basement,
sample.water_alt,
sample.humidity,
sample.temp,
sample.downhill,
sample.river.river_kind,
)
})
.unwrap_or((
CONFIG.sea_level,
CONFIG.sea_level,
CONFIG.sea_level,
0.0,
0.0,
None,
None,
));
let humidity = humidity.min(1.0).max(0.0);
let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5;
let pos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let downhill_pos = (downhill
.map(|downhill_pos| downhill_pos/*.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32)*/)
.unwrap_or(pos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32))
- pos)/* * scale*/
+ 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 alt_tl = sampler.get(top_left).map(|s| s.alt)
.unwrap_or(CONFIG.sea_level);
let alt_tr = sampler.get(top_right).map(|s| s.alt)
.unwrap_or(CONFIG.sea_level);
let alt_bl = sampler.get(bottom_left).map(|s| s.alt)
.unwrap_or(CONFIG.sea_level); */
let cross_pos = pos
+ ((downhill_pos - pos)
.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);
// 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,
);
// 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,
);
// Then cross points "to the right" (upwards) on a right-handed coordinate system.
// (right-handed coordinate system means (0, 0, 1.0) is "forward" into the screen).
let surface_normal = forward_vec.cross(up_vec).normalized();
// f = (0, alt_bl - alt_tl, 1) [backward right-handed = (0,0,1)]
// u = (1, alt_tr - alt_tl, 0) [right (90 degrees CCW backward) = (1,0,0)]
// (f × u in right-handed coordinate system: pointing up)
//
// f × u =
// (a.y*b.z - a.z*b.y,
// a.z*b.x - a.x*b.z,
// a.x*b.y - a.y*b.x,
// )
// =
// (-(alt_tr - alt_tl),
// 1,
// -(alt_bl - alt_tl),
// )
// =
// (alt_tl - alt_tr,
// 1,
// alt_tl - alt_bl,
// )
//
// let surface_normal = Vec3::new((alt_tl - alt_tr) as f64, 1.0, (alt_tl - alt_bl) as f64).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);
let quad =
|x: f32| ((x as f64 * QUADRANTS as f64).floor() as usize).min(QUADRANTS - 1);
if river_kind.is_none() || humidity != 0.0 {
quads[quad(humidity)][quad(temperature)] += 1;
}
match river_kind {
Some(RiverKind::River { .. }) => {
rivers += 1;
}
Some(RiverKind::Lake { .. }) => {
lakes += 1;
}
Some(RiverKind::Ocean { .. }) => {
oceans += 1;
}
None => {}
}
buf[j * W + i] = 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.2 + (alt * 0.8) } 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 };
u32::from_le_bytes([
(b * light * 255.0) as u8,
(g * light * 255.0) as u8,
(r * light * 255.0) as u8,
255,
])
/* u32::from_le_bytes([
(/*alt * *//*(1.0 - humidity)*/(alt * humidity).sqrt()/*temperature*/ * 255.0) as u8,
(/*alt*//*alt*//* * humidity*//*alt * 255.0*//*humidity*/alt * 255.0) as u8,
(/*alt*//*alt * *//*(1.0 - humidity)*/(alt * temperature).sqrt() * 255.0) as u8,
255,
]) */
}
(Some(RiverKind::Ocean), _) => u32::from_le_bytes([
((64.0 - water_depth * 64.0) * 1.0) as u8,
((32.0 - water_depth * 32.0) * 1.0) as u8,
0,
255,
]),
(Some(RiverKind::River { .. }), _) => u32::from_le_bytes([
64 + (alt * 191.0) as u8,
32 + (alt * 95.0) as u8,
0,
255,
]),
(None, _) | (Some(RiverKind::Lake { .. }), _) => u32::from_le_bytes([
(((64.0 + water_alt * 191.0) + (-water_depth * 64.0)) * 1.0) as u8,
(((32.0 + water_alt * 95.0) + (-water_depth * 32.0)) * 1.0) as u8,
0,
255,
]),
};
}
}
let MapDebug {
rivers,
lakes,
oceans,
quads,
} = config.generate(sampler, |pos, (r, g, b, a)| {
let i = pos.x;
let j = pos.y;
buf[j * W + i] = u32::from_le_bytes([b, g, r, a]);
});
let spd = 32.0;
let lspd = 0.1;
@ -299,8 +132,8 @@ fn main() {
}
if win.get_mouse_down(minifb::MouseButton::Left) {
if let Some((mx, my)) = win.get_mouse_pos(minifb::MouseMode::Clamp) {
let pos =
(focus_rect + (Vec2::new(mx as f64, my as f64) * scale)).map(|e| e as i32);
let pos = (Vec2::<f64>::from(focus) + (Vec2::new(mx as f64, my as f64) * scale))
.map(|e| e as i32);
println!(
"Chunk position: {:?}",
pos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e * f as i32)

315
world/src/sim/map.rs Normal file
View File

@ -0,0 +1,315 @@
use crate::{
sim::{self, uniform_idx_as_vec2, RiverKind, WorldSim, WORLD_SIZE},
util::Sampler,
CONFIG,
};
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
use std::{f32, f64};
use vek::*;
pub struct MapConfig {
/// Dimensions of the window being written to. Defaults to WORLD_SIZE.
pub dimensions: Vec2<usize>,
/// x, y, and z of top left of map (defaults to (0.0, 0.0, CONFIG.sea_level)).
pub focus: Vec3<f64>,
/// Altitude is divided by gain and clamped to [0, 1]; thus, decreasing gain makes
/// smaller differences in altitude appear larger.
///
/// Defaults to CONFIG.mountain_scale.
pub gain: f32,
/// lgain is used for shading purposes and refers to how much impact a change in the z
/// direction has on the perceived slope relative to the same change in x and y.
///
/// Defaults to TerrainChunkSize::RECT_SIZE.x.
pub lgain: f64,
/// Scale is like gain, but for x and y rather than z.
///
/// Defaults to WORLD_SIZE.x / dimensions.x (NOTE: fractional, not integer, division!).
pub scale: f64,
/// Vector that indicates which direction light is coming from, if shading is turned on.
///
/// Right-handed coordinate system: light is going left, down, and "backwards" (i.e. on the
/// map, where we translate the y coordinate on the world map to z in the coordinate system,
/// the light comes from -y on the map and points towards +y on the map). In a right
/// handed coordinate system, the "camera" points towards -z, so positive z is backwards
/// "into" the camera.
///
/// "In world space the x-axis will be pointing east, the y-axis up and the z-axis will be pointing south"
///
/// Defaults to (-0.8, -1.0, 0.3).
pub light_direction: Vec3<f64>,
/// If true, only the basement (bedrock) is used for altitude; otherwise, the surface is used.
///
/// Defaults to false.
pub is_basement: bool,
/// If true, water is rendered; otherwise, the surface without water is rendered, even if it
/// is underwater.
///
/// Defaults to true.
pub is_water: bool,
/// If true, 3D lighting and shading are turned on. Otherwise, a plain altitude map is used.
///
/// Defaults to true.
pub is_shaded: bool,
/// If true, the red component of the image is also used for temperature (redder is hotter).
/// Defaults to false.
pub is_temperature: bool,
/// If true, the blue component of the image is also used for humidity (bluer is wetter).
///
/// Defaults to false.
pub is_humidity: bool,
/// Record debug information.
///
/// Defaults to false.
pub is_debug: bool,
}
pub const QUADRANTS: usize = 4;
pub struct MapDebug {
pub quads: [[u32; QUADRANTS]; QUADRANTS],
pub rivers: u32,
pub lakes: u32,
pub oceans: u32,
}
impl Default for MapConfig {
fn default() -> Self {
let dimensions = WORLD_SIZE;
Self {
dimensions,
focus: Vec3::new(0.0, 0.0, CONFIG.sea_level as f64),
gain: CONFIG.mountain_scale,
lgain: TerrainChunkSize::RECT_SIZE.x as f64,
scale: WORLD_SIZE.x as f64 / dimensions.x as f64,
light_direction: Vec3::new(-0.8, -1.0, 0.3),
is_basement: false,
is_water: true,
is_shaded: true,
is_temperature: false,
is_humidity: false,
is_debug: false,
}
}
}
impl MapConfig {
/// 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 {
let MapConfig {
dimensions,
focus,
gain,
lgain,
scale,
light_direction,
is_basement,
is_water,
is_shaded,
is_temperature,
is_humidity,
is_debug,
} = *self;
let light = light_direction.normalized();
let mut quads = [[0u32; QUADRANTS]; QUADRANTS];
let mut rivers = 0u32;
let mut lakes = 0u32;
let mut oceans = 0u32;
// let water_light = (light_direction.z + 1.0) / 2.0 * 0.8 + 0.2;
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 pos = uniform_idx_as_vec2(chunk_idx);
let i = pos.x as usize;
let j = pos.y as usize;
let pos =
(focus_rect + Vec2::new(i as f64, j as f64) * scale).map(|e: f64| e as i32);
let (alt, basement, water_alt, humidity, temperature, downhill, river_kind) =
sampler
.get(pos)
.map(|sample| {
(
sample.alt,
sample.basement,
sample.water_alt,
sample.humidity,
sample.temp,
sample.downhill,
sample.river.river_kind,
)
})
.unwrap_or((
CONFIG.sea_level,
CONFIG.sea_level,
CONFIG.sea_level,
0.0,
0.0,
None,
None,
));
let humidity = humidity.min(1.0).max(0.0);
let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5;
let pos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let downhill_pos = (downhill
.map(|downhill_pos| downhill_pos/*.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32)*/)
.unwrap_or(pos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32))
- pos)/* * scale*/
+ 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)
.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);
// 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,
);
// 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,
);
// Then cross points "to the right" (upwards) on a right-handed coordinate system.
// (right-handed coordinate system means (0, 0, 1.0) is "forward" into the screen).
let surface_normal = forward_vec.cross(up_vec).normalized();
// f = (0, alt_bl - alt_tl, 1) [backward right-handed = (0,0,1)]
// u = (1, alt_tr - alt_tl, 0) [right (90 degrees CCW backward) = (1,0,0)]
// (f × u in right-handed coordinate system: pointing up)
//
// f × u =
// (a.y*b.z - a.z*b.y,
// a.z*b.x - a.x*b.z,
// a.x*b.y - a.y*b.x,
// )
// =
// (-(alt_tr - alt_tl),
// 1,
// -(alt_bl - alt_tl),
// )
// =
// (alt_tl - alt_tr,
// 1,
// alt_tl - alt_bl,
// )
//
// let surface_normal = Vec3::new((alt_tl - alt_tr) as f64, 1.0, (alt_tl - alt_bl) as f64).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 {
let quad =
|x: f32| ((x as f64 * QUADRANTS as f64).floor() as usize).min(QUADRANTS - 1);
if river_kind.is_none() || humidity != 0.0 {
quads[quad(humidity)][quad(temperature)] += 1;
}
match river_kind {
Some(RiverKind::River { .. }) => {
rivers += 1;
}
Some(RiverKind::Lake { .. }) => {
lakes += 1;
}
Some(RiverKind::Ocean { .. }) => {
oceans += 1;
}
None => {}
}
}
let rgba = 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.2 + (alt * 0.8) } 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,
255,
)
}
(Some(RiverKind::Ocean), _) => (
0,
((32.0 - water_depth * 32.0) * 1.0) as u8,
((64.0 - water_depth * 64.0) * 1.0) as u8,
255,
),
(Some(RiverKind::River { .. }), _) => (
0,
32 + (alt * 95.0) as u8,
64 + (alt * 191.0) as u8,
255,
),
(None, _) | (Some(RiverKind::Lake { .. }), _) => (
0,
(((32.0 + water_alt * 95.0) + (-water_depth * 32.0)) * 1.0) as u8,
(((64.0 + water_alt * 191.0) + (-water_depth * 64.0)) * 1.0) as u8,
255,
),
};
write_pixel(Vec2::new(i, j), rgba);
});
MapDebug {
quads,
rivers,
lakes,
oceans,
}
}
}

View File

@ -1,6 +1,7 @@
mod diffusion;
mod erosion;
mod location;
mod map;
mod settlement;
mod util;
@ -12,6 +13,7 @@ pub use self::erosion::{
mrec_downhill, Alt, RiverData, RiverKind,
};
pub use self::location::Location;
pub use self::map::{MapConfig, MapDebug};
pub use self::settlement::Settlement;
pub use self::util::{
cdf_irwin_hall, downhill, get_oceans, local_cells, map_edge_factor, neighbors,
@ -1651,39 +1653,12 @@ impl WorldSim {
/// Draw a map of the world based on chunk information. Returns a buffer of u32s.
pub fn get_map(&self) -> Vec<u32> {
(0..WORLD_SIZE.x * WORLD_SIZE.y)
.into_par_iter()
.map(|chunk_idx| {
let pos = uniform_idx_as_vec2(chunk_idx);
let (alt, water_alt, river_kind) = self
.get(pos)
.map(|sample| (sample.alt, sample.water_alt, sample.river.river_kind))
.unwrap_or((CONFIG.sea_level, CONFIG.sea_level, None));
let alt = ((alt - CONFIG.sea_level) / CONFIG.mountain_scale)
.min(1.0)
.max(0.0);
let water_alt = ((alt.max(water_alt) - CONFIG.sea_level) / CONFIG.mountain_scale)
.min(1.0)
.max(0.0);
match river_kind {
Some(RiverKind::Ocean) => u32::from_le_bytes([0, 32, 64, 255]),
Some(RiverKind::Lake { .. }) => u32::from_le_bytes([
0,
32 + (water_alt * 95.0) as u8,
64 + (water_alt * 191.0) as u8,
255,
]),
Some(RiverKind::River { .. }) => u32::from_le_bytes([
0,
32 + (alt * 95.0) as u8,
64 + (alt * 191.0) as u8,
255,
]),
None => u32::from_le_bytes([0, (alt * 255.0) as u8, 0, 255]),
}
})
.collect()
let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y];
// TODO: Parallelize again.
MapConfig::default().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
@ -1825,7 +1800,7 @@ impl WorldSim {
chunk_idx_center(WORLD_SIZE.map(|e| e as i32)),
)
.map_init(
|| BlockGen::new(ColumnGen::new(self)),
|| Box::new(BlockGen::new(ColumnGen::new(self))),
|mut block_gen, (pos, seed)| {
let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
// println!("Town: {:?}", town);