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 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            // 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 (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}