mirror of
https://github.com/neocturne/MinedMap.git
synced 2025-04-19 02:55:08 +02:00
341 lines
9.3 KiB
Rust
341 lines
9.3 KiB
Rust
//! The [TileRenderer] and related types and functions
|
|
|
|
use std::{
|
|
num::NonZeroUsize,
|
|
path::PathBuf,
|
|
sync::{Arc, Mutex},
|
|
time::SystemTime,
|
|
};
|
|
|
|
use anyhow::{Context, Result};
|
|
use lru::LruCache;
|
|
use rayon::prelude::*;
|
|
use tokio::sync::OnceCell;
|
|
use tracing::{debug, info};
|
|
|
|
use super::{common::*, region_group::RegionGroup};
|
|
use crate::{
|
|
io::{fs, storage},
|
|
resource::{Colorf, block_color, needs_biome},
|
|
types::*,
|
|
util::coord_offset,
|
|
};
|
|
|
|
/// Type for referencing loaded [ProcessedRegion] data
|
|
type RegionRef = Arc<ProcessedRegion>;
|
|
|
|
/// Returns the index of the biome at a block coordinate
|
|
///
|
|
/// The passed chunk and block coordinates relative to the center of the
|
|
/// region group is offset by *dx* and *dz*.
|
|
///
|
|
/// The returned tuple contains the relative region coordinates the offset coordinate
|
|
/// ends up in (in the range -1..1) and the index in that region's biome list.
|
|
fn biome_at(
|
|
region_group: &RegionGroup<RegionRef>,
|
|
chunk: ChunkCoords,
|
|
block: LayerBlockCoords,
|
|
dx: i32,
|
|
dz: i32,
|
|
) -> Option<(i8, i8, u16)> {
|
|
let (region_x, chunk_x, block_x) = coord_offset(chunk.x, block.x, dx);
|
|
let (region_z, chunk_z, block_z) = coord_offset(chunk.z, block.z, dz);
|
|
let chunk = ChunkCoords {
|
|
x: chunk_x,
|
|
z: chunk_z,
|
|
};
|
|
let block = LayerBlockCoords {
|
|
x: block_x,
|
|
z: block_z,
|
|
};
|
|
let region = region_group.get(region_x, region_z)?;
|
|
Some((
|
|
region_x,
|
|
region_z,
|
|
region.chunks[chunk].as_ref()?.biomes[block]?.get() - 1,
|
|
))
|
|
}
|
|
|
|
/// The TileRenderer generates map tiles from processed region data
|
|
pub struct TileRenderer<'a> {
|
|
/// Common MinedMap configuration from command line
|
|
config: &'a Config,
|
|
/// Runtime for asynchronous region loading
|
|
rt: &'a tokio::runtime::Runtime,
|
|
/// List of populated regions to render tiles for
|
|
regions: &'a [TileCoords],
|
|
/// Set of populated regions for fast existence checking
|
|
region_set: rustc_hash::FxHashSet<TileCoords>,
|
|
/// Cache of previously loaded regions
|
|
region_cache: Mutex<LruCache<PathBuf, Arc<OnceCell<RegionRef>>>>,
|
|
}
|
|
|
|
impl<'a> TileRenderer<'a> {
|
|
/// Constructs a new TileRenderer
|
|
pub fn new(
|
|
config: &'a Config,
|
|
rt: &'a tokio::runtime::Runtime,
|
|
regions: &'a [TileCoords],
|
|
) -> Self {
|
|
let region_cache = Mutex::new(LruCache::new(
|
|
NonZeroUsize::new(6 + 6 * config.num_threads).unwrap(),
|
|
));
|
|
let region_set = regions.iter().copied().collect();
|
|
TileRenderer {
|
|
config,
|
|
rt,
|
|
regions,
|
|
region_set,
|
|
region_cache,
|
|
}
|
|
}
|
|
|
|
/// Loads [ProcessedRegion] for a region or returns previously loaded data from the region cache
|
|
async fn load_region(&self, processed_path: PathBuf) -> Result<RegionRef> {
|
|
let region_loader = {
|
|
let mut region_cache = self.region_cache.lock().unwrap();
|
|
if let Some(region_loader) = region_cache.get(&processed_path) {
|
|
Arc::clone(region_loader)
|
|
} else {
|
|
let region_loader = Default::default();
|
|
region_cache.put(processed_path.clone(), Arc::clone(®ion_loader));
|
|
region_loader
|
|
}
|
|
};
|
|
|
|
region_loader
|
|
.get_or_try_init(|| async {
|
|
storage::read_file(&processed_path).context("Failed to load processed region data")
|
|
})
|
|
.await
|
|
.cloned()
|
|
}
|
|
|
|
/// Loads a 3x3 neighborhood of processed region data
|
|
async fn load_region_group(
|
|
&self,
|
|
processed_paths: RegionGroup<PathBuf>,
|
|
) -> Result<RegionGroup<RegionRef>> {
|
|
processed_paths
|
|
.async_try_map(move |path| self.load_region(path))
|
|
.await
|
|
}
|
|
|
|
/// Computes the color of a tile pixel
|
|
fn block_color_at(
|
|
region_group: &RegionGroup<RegionRef>,
|
|
chunk: &ProcessedChunk,
|
|
chunk_coords: ChunkCoords,
|
|
block_coords: LayerBlockCoords,
|
|
) -> Option<Colorf> {
|
|
/// Helper for keys in the weight table
|
|
///
|
|
/// Hashing the value as a single u32 is more efficient than hashing
|
|
/// the tuple elements separately.
|
|
fn biome_key((dx, dz, index): (i8, i8, u16)) -> u32 {
|
|
(dx as u8 as u32) | ((dz as u8 as u32) << 8) | ((index as u32) << 16)
|
|
}
|
|
|
|
/// One quadrant of the kernel used to smooth biome edges
|
|
///
|
|
/// The kernel is mirrored in X und Z direction to build the full 5x5
|
|
/// smoothing kernel.
|
|
const SMOOTH: [[f32; 3]; 3] = [[41.0, 26.0, 7.0], [26.0, 16.0, 4.0], [7.0, 4.0, 1.0]];
|
|
/// Maximum X coordinate offset to take into account for biome smoothing
|
|
const X: isize = SMOOTH[0].len() as isize - 1;
|
|
/// Maximum Z coordinate offset to take into account for biome smoothing
|
|
const Z: isize = SMOOTH.len() as isize - 1;
|
|
|
|
let block = chunk.blocks[block_coords]?;
|
|
let depth = chunk.depths[block_coords]?;
|
|
|
|
if !needs_biome(block) {
|
|
return Some(block_color(block, None, depth.0 as f32));
|
|
}
|
|
|
|
let mut weights = rustc_hash::FxHashMap::<u32, ((i8, i8, u16), f32)>::default();
|
|
for dz in -Z..=Z {
|
|
for dx in -X..=X {
|
|
let w = SMOOTH[dz.unsigned_abs()][dx.unsigned_abs()];
|
|
if w == 0.0 {
|
|
continue;
|
|
}
|
|
|
|
let Some(biome) = biome_at(
|
|
region_group,
|
|
chunk_coords,
|
|
block_coords,
|
|
dx as i32,
|
|
dz as i32,
|
|
) else {
|
|
continue;
|
|
};
|
|
|
|
let value = weights.entry(biome_key(biome)).or_default();
|
|
value.0 = biome;
|
|
value.1 += w;
|
|
}
|
|
}
|
|
|
|
if weights.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let mut color = Colorf::ZERO;
|
|
let mut total = 0.0;
|
|
|
|
for ((region_x, region_z, index), w) in weights.into_values() {
|
|
let region = region_group.get(region_x, region_z)?;
|
|
let biome = region.biome_list.get(usize::from(index))?;
|
|
|
|
total += w;
|
|
color += w * block_color(block, Some(biome), depth.0 as f32);
|
|
}
|
|
|
|
Some(color / total)
|
|
}
|
|
|
|
/// Renders a chunk subtile into a region tile image
|
|
fn render_chunk(
|
|
image: &mut image::RgbaImage,
|
|
region_group: &RegionGroup<RegionRef>,
|
|
chunk: &ProcessedChunk,
|
|
chunk_coords: ChunkCoords,
|
|
) {
|
|
/// Width/height of a chunk subtile
|
|
const N: u32 = BLOCKS_PER_CHUNK as u32;
|
|
|
|
let chunk_image = image::RgbaImage::from_fn(N, N, |x, z| {
|
|
let block_coords = LayerBlockCoords {
|
|
x: BlockX::new(x),
|
|
z: BlockZ::new(z),
|
|
};
|
|
let color = Self::block_color_at(region_group, chunk, chunk_coords, block_coords);
|
|
image::Rgba(
|
|
color
|
|
.map(|c| [c[0] as u8, c[1] as u8, c[2] as u8, 255])
|
|
.unwrap_or_default(),
|
|
)
|
|
});
|
|
overlay_chunk(image, &chunk_image, chunk_coords);
|
|
}
|
|
|
|
/// Renders a region tile image
|
|
fn render_region(image: &mut image::RgbaImage, region_group: &RegionGroup<RegionRef>) {
|
|
for (coords, chunk) in region_group.center().chunks.iter() {
|
|
let Some(chunk) = chunk else {
|
|
continue;
|
|
};
|
|
|
|
Self::render_chunk(image, region_group, chunk, coords);
|
|
}
|
|
}
|
|
|
|
/// Returns the filename of the processed data for a region and the time of its last modification
|
|
fn processed_source(&self, coords: TileCoords) -> Result<(PathBuf, SystemTime)> {
|
|
let path = self.config.processed_path(coords);
|
|
let timestamp = fs::modified_timestamp(&path)?;
|
|
Ok((path, timestamp))
|
|
}
|
|
|
|
/// Returns the filenames of the processed data for a 3x3 neighborhood of a region
|
|
/// and the time of last modification for any of them
|
|
fn processed_sources(&self, coords: TileCoords) -> Result<(RegionGroup<PathBuf>, SystemTime)> {
|
|
let sources = RegionGroup::new(|x, z| {
|
|
Some(TileCoords {
|
|
x: coords.x + (x as i32),
|
|
z: coords.z + (z as i32),
|
|
})
|
|
.filter(|entry| self.region_set.contains(entry))
|
|
})
|
|
.try_map(|entry| self.processed_source(entry))
|
|
.with_context(|| format!("Region {:?} from previous step must exist", coords))?;
|
|
|
|
let max_timestamp = *sources
|
|
.iter()
|
|
.map(|(_, timestamp)| timestamp)
|
|
.max()
|
|
.expect("at least one timestamp must exist");
|
|
|
|
let paths = sources.map(|(path, _)| path);
|
|
Ok((paths, max_timestamp))
|
|
}
|
|
|
|
/// Renders and saves a region tile image
|
|
fn render_tile(&self, coords: TileCoords) -> Result<bool> {
|
|
/// Width/height of a tile image
|
|
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
|
|
|
|
let (processed_paths, processed_timestamp) = self.processed_sources(coords)?;
|
|
|
|
let output_path = self.config.tile_path(TileKind::Map, 0, coords);
|
|
let output_timestamp = fs::read_timestamp(&output_path, MAP_FILE_META_VERSION);
|
|
|
|
if Some(processed_timestamp) <= output_timestamp {
|
|
debug!(
|
|
"Skipping unchanged tile {}",
|
|
output_path
|
|
.strip_prefix(&self.config.output_dir)
|
|
.expect("tile path must be in output directory")
|
|
.display(),
|
|
);
|
|
return Ok(false);
|
|
}
|
|
|
|
debug!(
|
|
"Rendering tile {}",
|
|
output_path
|
|
.strip_prefix(&self.config.output_dir)
|
|
.expect("tile path must be in output directory")
|
|
.display(),
|
|
);
|
|
|
|
let region_group = self
|
|
.rt
|
|
.block_on(self.load_region_group(processed_paths))
|
|
.with_context(|| format!("Region {:?} from previous step must be loadable", coords))?;
|
|
let mut image = image::RgbaImage::new(N, N);
|
|
Self::render_region(&mut image, ®ion_group);
|
|
|
|
fs::create_with_timestamp(
|
|
&output_path,
|
|
MAP_FILE_META_VERSION,
|
|
processed_timestamp,
|
|
|file| {
|
|
image
|
|
.write_to(file, self.config.tile_image_format())
|
|
.context("Failed to save image")
|
|
},
|
|
)?;
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Runs the tile generation
|
|
pub fn run(self) -> Result<()> {
|
|
fs::create_dir_all(&self.config.tile_dir(TileKind::Map, 0))?;
|
|
|
|
info!("Rendering map tiles...");
|
|
|
|
// Use par_bridge to process items in order (for better use of region cache)
|
|
let processed = self
|
|
.regions
|
|
.iter()
|
|
.par_bridge()
|
|
.map(|&coords| {
|
|
anyhow::Ok(usize::from(
|
|
self.render_tile(coords)
|
|
.with_context(|| format!("Failed to render tile {:?}", coords))?,
|
|
))
|
|
})
|
|
.try_reduce(|| 0, |a, b| Ok(a + b))?;
|
|
|
|
info!(
|
|
"Rendered map tiles ({} processed, {} unchanged)",
|
|
processed,
|
|
self.regions.len() - processed,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|