minetest_worldmapper/
render.rs

1use crate::{
2    color::Color, config::Config, mapblock::analyze_positions, mapblock::compute_mapblock,
3    mapblock::CHUNK_SIZE, terrain::Terrain, terrain::TerrainCell,
4};
5use futures::future::join_all;
6use image::RgbaImage;
7use minetestworld::MAPBLOCK_LENGTH;
8use minetestworld::{MapData, Position};
9use std::collections::BinaryHeap;
10use std::error::Error;
11use std::sync::Arc;
12use tokio::task;
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            // An error here is noted, but the rendering continues
28            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
37/// Renders the surface colors of the terrain along with its heightmap
38pub 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 chunk in chunks.drain(..) {
62        match chunk {
63            Ok((x, z, chunk)) => {
64                let offset_x = (base_offset.0 + MAPBLOCK_LENGTH as i16 * x) as u32;
65                let offset_z = (base_offset.1 - MAPBLOCK_LENGTH as i16 * (z + 1)) as u32;
66                terrain.insert_chunk((offset_x, offset_z), chunk)
67            }
68            Err(e) => log::error!("Error generating terrain map: {e}"),
69        }
70    }
71    Ok(terrain)
72}
73
74#[derive(thiserror::Error, Debug)]
75pub enum RenderingError {
76    #[error("width has to fit into u32")]
77    WidthTooBig(std::num::TryFromIntError),
78    #[error("height has to fit into u32")]
79    HeightTooBig(std::num::TryFromIntError),
80}
81
82fn shade(color: &mut Color, height_diff: i16) {
83    if height_diff < 0 {
84        let descent: u8 = (-height_diff).try_into().unwrap_or(0);
85        color.darken(descent.saturating_mul(2));
86    }
87    if height_diff > 0 {
88        let ascent: u8 = height_diff.try_into().unwrap_or(0);
89        color.lighten_up(ascent.saturating_mul(2));
90    }
91}
92
93impl Terrain {
94    fn heightdiff(&self, x: u32, y: u32) -> i16 {
95        let x_diff = self.height_diff_x(x, y).unwrap_or(0);
96        let y_diff = self.height_diff_y(x, y).unwrap_or(0);
97        x_diff + y_diff
98    }
99
100    pub fn render(&self, config: &Config) -> Result<RgbaImage, RenderingError> {
101        let mut image = RgbaImage::new(
102            self.width()
103                .try_into()
104                .map_err(RenderingError::WidthTooBig)?,
105            self.height()
106                .try_into()
107                .map_err(RenderingError::HeightTooBig)?,
108        );
109        for y in 0..self.height() {
110            let y = y as u32;
111            for x in 0..self.width() {
112                let x = x as u32;
113                let mut col = self.get_color(x, y).unwrap_or(config.background_color);
114                if config.hill_shading.enabled {
115                    shade(&mut col, self.heightdiff(x, y));
116                }
117                *image.get_pixel_mut(x, y) = col.with_background(&config.background_color).0;
118            }
119        }
120        Ok(image)
121    }
122}