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