minetest_worldmapper/
render.rs1use crate::{
2 color::Color, config::Config, mapblock::analyze_positions, mapblock::compute_mapblock,
3 mapblock::CHUNK_SIZE, terrain::Terrain, terrain::TerrainCell,
4};
5use async_std::task;
6use futures::future::join_all;
7use image::RgbaImage;
8use minetestworld::MAPBLOCK_LENGTH;
9use minetestworld::{MapData, Position};
10use std::collections::BinaryHeap;
11use std::error::Error;
12use std::sync::Arc;
13
14async fn generate_terrain_chunk(
15 config: Arc<Config>,
16 map: Arc<MapData>,
17 x: i16,
18 z: i16,
19 mut ys: BinaryHeap<i16>,
20) -> (i16, i16, [TerrainCell; CHUNK_SIZE]) {
21 let mut chunk = [TerrainCell::default(); CHUNK_SIZE];
22 while let Some(y) = ys.pop() {
23 match map.get_mapblock(Position { x, y, z }).await {
24 Ok(mapblock) => {
25 compute_mapblock(&mapblock, &config, y * MAPBLOCK_LENGTH as i16, &mut chunk)
26 }
27 Err(e) => log::error!("Error reading mapblock at {x},{y},{z}: {e}"),
29 }
30 if chunk.iter().all(|c| c.alpha() > config.sufficient_alpha) {
31 break;
32 }
33 }
34 (x, z, chunk)
35}
36
37pub async fn compute_terrain(map: MapData, config: &Config) -> Result<Terrain, Box<dyn Error>> {
39 let mapblock_positions = map.all_mapblock_positions().await;
40 let (mut xz_positions, bbox) = analyze_positions(mapblock_positions).await?;
41 log::info!("{bbox:?}");
42 let mut terrain = Terrain::new(
43 MAPBLOCK_LENGTH as usize * bbox.x.len(),
44 MAPBLOCK_LENGTH as usize * bbox.z.len() + 1,
45 );
46 let base_offset = (
47 -bbox.x.start * MAPBLOCK_LENGTH as i16,
48 bbox.z.end * MAPBLOCK_LENGTH as i16,
49 );
50
51 let config = Arc::new(config.clone());
52 let map = Arc::new(map);
53 let mut chunks = join_all(xz_positions.drain().map(|((x, z), ys)| {
54 let config = config.clone();
55 let map = map.clone();
56 task::spawn(generate_terrain_chunk(config, map, x, z, ys))
57 }))
58 .await;
59
60 log::info!("Finishing surface map");
61 for (x, z, chunk) in chunks.drain(..) {
62 let offset_x = (base_offset.0 + MAPBLOCK_LENGTH as i16 * x) as u32;
63 let offset_z = (base_offset.1 - MAPBLOCK_LENGTH as i16 * (z + 1)) as u32;
64 terrain.insert_chunk((offset_x, offset_z), chunk)
65 }
66 Ok(terrain)
67}
68
69#[derive(thiserror::Error, Debug)]
70pub enum RenderingError {
71 #[error("width has to fit into u32")]
72 WidthTooBig(std::num::TryFromIntError),
73 #[error("height has to fit into u32")]
74 HeightTooBig(std::num::TryFromIntError),
75}
76
77fn shade(color: &mut Color, height_diff: i16) {
78 if height_diff < 0 {
79 let descent: u8 = (-height_diff).try_into().unwrap_or(0);
80 color.darken(descent.saturating_mul(2));
81 }
82 if height_diff > 0 {
83 let ascent: u8 = height_diff.try_into().unwrap_or(0);
84 color.lighten_up(ascent.saturating_mul(2));
85 }
86}
87
88impl Terrain {
89 fn heightdiff(&self, x: u32, y: u32) -> i16 {
90 let x_diff = self.height_diff_x(x, y).unwrap_or(0);
91 let y_diff = self.height_diff_y(x, y).unwrap_or(0);
92 x_diff + y_diff
93 }
94
95 pub fn render(&self, config: &Config) -> Result<RgbaImage, RenderingError> {
96 let mut image = RgbaImage::new(
97 self.width()
98 .try_into()
99 .map_err(RenderingError::WidthTooBig)?,
100 self.height()
101 .try_into()
102 .map_err(RenderingError::HeightTooBig)?,
103 );
104 for y in 0..self.height() {
105 let y = y as u32;
106 for x in 0..self.width() {
107 let x = x as u32;
108 let mut col = self.get_color(x, y).unwrap_or(config.background_color);
109 if config.hill_shading.enabled {
110 shade(&mut col, self.heightdiff(x, y));
111 }
112 *image.get_pixel_mut(x, y) = col.with_background(&config.background_color).0;
113 }
114 }
115 Ok(image)
116 }
117}