diff options
Diffstat (limited to 'src/proc_gen')
| -rw-r--r-- | src/proc_gen/herringbone.rs | 83 | ||||
| -rw-r--r-- | src/proc_gen/mod.rs | 114 | ||||
| -rw-r--r-- | src/proc_gen/tileset_loader.rs | 204 |
3 files changed, 312 insertions, 89 deletions
diff --git a/src/proc_gen/herringbone.rs b/src/proc_gen/herringbone.rs new file mode 100644 index 0000000..5e930ec --- /dev/null +++ b/src/proc_gen/herringbone.rs @@ -0,0 +1,83 @@ +use fxhash::hash32; +use glam::IVec2; + +use crate::{ + config::TILESET_SCALING, + content::materials::MaterialId, + proc_gen::tileset_loader::{Tile, TileOrientation, Tileset, TilesetPixelType}, + sim::cell::Cell, +}; + +fn split_position(world: IVec2, tileset: &Tileset) -> (IVec2, IVec2, TileOrientation) { + let s = tileset.dimensions.short as i32; + let tx = world.x.div_euclid(s); + let ty = world.y.div_euclid(s); + let fx = world.x.rem_euclid(s); + let fy = world.y.rem_euclid(s); + + match (tx - ty).rem_euclid(4) { + // left half of horizontal tile + 0 => ( + IVec2::new(tx, ty), + IVec2::new(fx, fy), + TileOrientation::Horizontal, + ), + // right half of horizontal tile + 1 => ( + IVec2::new(tx - 1, ty), + IVec2::new(fx + s, fy), + TileOrientation::Horizontal, + ), + // top half of vertical tile + 3 => ( + IVec2::new(tx, ty), + IVec2::new(fx, fy), + TileOrientation::Vertical, + ), + // bottom half of vertical tile + 2 => ( + IVec2::new(tx, ty - 1), + IVec2::new(fx, fy + s), + TileOrientation::Vertical, + ), + _ => unreachable!(), + } +} + +// better "%" for hashes since it prefers the high bits +#[inline] +fn reduce(h: u32, n: usize) -> usize { + (((h as u128) * (n as u128)) >> 32) as usize +} + +fn pick_variant(tile_pos: IVec2, orientation: TileOrientation, tileset: &Tileset) -> &Tile { + let h = hash32(&tile_pos); + match orientation { + TileOrientation::Horizontal => { + &tileset.horizontal_tiles[reduce(h, tileset.horizontal_tiles.len())] + } + TileOrientation::Vertical => { + &tileset.vertical_tiles[reduce(h, tileset.vertical_tiles.len())] + } + } +} + +// TODO: optimize +fn get_pixel_at_position(world: IVec2, tileset: &Tileset) -> TilesetPixelType { + let scaled_world = world.div_euclid(IVec2::new(TILESET_SCALING, TILESET_SCALING)); + let (tile_pos, local, orientation) = split_position(scaled_world, tileset); + let tile = pick_variant(tile_pos, orientation, tileset); + let width = match orientation { + TileOrientation::Horizontal => tileset.dimensions.long as i32, + TileOrientation::Vertical => tileset.dimensions.short as i32, + }; + tile.pixels[(local.x + local.y * width) as usize] +} + +pub fn get_cell_at_position(world: IVec2, tileset: &Tileset) -> Cell { + let pixel = get_pixel_at_position(world, tileset); + match pixel { + TilesetPixelType::Void => Cell::void(), + TilesetPixelType::Terrain => Cell::from_material(MaterialId::Dirt), + } +} diff --git a/src/proc_gen/mod.rs b/src/proc_gen/mod.rs index fb6a326..7913d22 100644 --- a/src/proc_gen/mod.rs +++ b/src/proc_gen/mod.rs @@ -1,101 +1,37 @@ -use fastnoise_lite::{FastNoiseLite, NoiseType}; -use glam::{IVec2, Vec2}; +pub mod herringbone; +pub mod tileset_loader; + +use glam::IVec2; use crate::{ config::CHUNK_SIZE, - content::materials::MaterialId, - sim::{cell::Cell, cell_manager::chunk::Chunk}, + proc_gen::{ + herringbone::get_cell_at_position, + tileset_loader::{Tileset, load_tileset}, + }, + sim::cell_manager::chunk::Chunk, }; -#[derive(Clone, Copy)] -pub struct WorldGeneratorConfig { - pub occupied_threshold: f32, - pub stone_threshold: f32, - pub proc_gen_seed: i32, - - pub cave_noise_freq: f32, - pub cave_noise_type: NoiseType, - pub hardness_noise_freq: f32, - pub hardness_noise_type: NoiseType, -} - -impl Default for WorldGeneratorConfig { - fn default() -> Self { - Self { - occupied_threshold: 0.05, - stone_threshold: 0.6, - proc_gen_seed: 10, - - cave_noise_freq: 0.005, - cave_noise_type: NoiseType::OpenSimplex2S, - hardness_noise_freq: 0.015, - hardness_noise_type: NoiseType::OpenSimplex2S, - } - } -} - -pub struct WorldGenerator { - config: WorldGeneratorConfig, - cave_noise: Option<FastNoiseLite>, - hardness_noise: Option<FastNoiseLite>, +pub struct TilesetWorldGenerator { + tileset: Tileset, } -impl WorldGenerator { - pub fn generate_chunk(&mut self, chunk_position: IVec2) -> Chunk { - if let Some(cave_noise) = &self.cave_noise - && let Some(hardness_noise) = &self.hardness_noise - { - let mut c = Chunk::void(); - let position = chunk_position * CHUNK_SIZE; - for x in 0..CHUNK_SIZE { - for y in 0..CHUNK_SIZE { - let cell_position = position.as_vec2() + Vec2::new(x as f32, y as f32); - let cnv = cave_noise.get_noise_2d(cell_position.x, cell_position.y); - let hnv = hardness_noise.get_noise_2d(cell_position.x, cell_position.y); - - let occupied = cnv < self.config.occupied_threshold; - if occupied { - let material = if hnv > self.config.stone_threshold { - MaterialId::Stone - } else { - MaterialId::Dirt - }; - c.set_cell_at_local_position( - x as u8, - y as u8, - Cell::from_material(material), - ); - } - } +impl TilesetWorldGenerator { + pub fn generate_chunk(&self, chunk_position: IVec2) -> Chunk { + let mut chunk = Chunk::void(); + for x in 0..CHUNK_SIZE { + for y in 0..CHUNK_SIZE { + let cell = get_cell_at_position( + chunk_position * CHUNK_SIZE + IVec2::new(x, y), + &self.tileset, + ); + chunk.set_cell_at_local_position(x as u8, y as u8, cell); } - c - } else { - panic!("Call update_params first"); } + chunk } - - pub fn update_params(&mut self, config: WorldGeneratorConfig) { - let mut cave_noise = FastNoiseLite::with_seed(config.proc_gen_seed + 50); - cave_noise.set_frequency(Some(config.cave_noise_freq)); - cave_noise.set_noise_type(Some(config.cave_noise_type)); - - let mut hardness_noise = FastNoiseLite::with_seed(config.proc_gen_seed + 100); - hardness_noise.set_frequency(Some(config.hardness_noise_freq)); - hardness_noise.set_noise_type(Some(config.hardness_noise_type)); - - self.cave_noise = Some(cave_noise); - self.hardness_noise = Some(hardness_noise); - - self.config = config; - } - - pub fn new(config: WorldGeneratorConfig) -> Self { - let mut wg = WorldGenerator { - config, - cave_noise: None, - hardness_noise: None, - }; - wg.update_params(config); - wg + pub fn new(tileset_path: &str) -> Self { + let tileset = load_tileset(tileset_path); + TilesetWorldGenerator { tileset } } } diff --git a/src/proc_gen/tileset_loader.rs b/src/proc_gen/tileset_loader.rs new file mode 100644 index 0000000..925b3ac --- /dev/null +++ b/src/proc_gen/tileset_loader.rs @@ -0,0 +1,204 @@ +use serde::Deserialize; +use std::{collections::HashMap, error::Error, fs::File, io::BufReader, path::Path}; + +fn load_img(path: &Path) -> Result<(Vec<u8>, u32, u32), Box<dyn Error>> { + let mut decoder = png::Decoder::new(BufReader::new(File::open(path)?)); + decoder.set_transformations(png::Transformations::IDENTITY); + + let mut reader = decoder.read_info()?; + // only None if it's too big for memory + let mut buf = vec![0; reader.output_buffer_size().unwrap()]; + let info = reader.next_frame(&mut buf)?; + + if info.color_type != png::ColorType::Indexed { + return Err(format!("{:#?}: not indexed", path.to_str()).into()); + } + if info.bit_depth != png::BitDepth::Eight { + return Err(format!( + "{:#?}: expected 8-bit, got {:?}", + path.to_str(), + info.bit_depth + ) + .into()); + } + + buf.truncate(info.buffer_size()); + Ok((buf, info.width, info.height)) +} + +#[derive(Deserialize, Debug)] +pub struct TilesetDimensions { + pub short: u32, + pub long: u32, +} + +#[derive(Deserialize, Clone, Copy, Debug)] +pub enum TilesetPixelType { + Void = 0, + Terrain, +} + +#[derive(Deserialize, Debug)] +struct TilesetManifest { + dimensions: TilesetDimensions, + palette: HashMap<u8, TilesetPixelType>, +} + +fn load_manifest(path: &Path) -> Result<TilesetManifest, Box<dyn Error>> { + Ok(toml::from_str(&std::fs::read_to_string(path)?)?) +} + +#[derive(Debug)] +pub struct Tile { + pub pixels: Vec<TilesetPixelType>, +} + +#[derive(Debug)] +pub struct Tileset { + pub dimensions: TilesetDimensions, + pub vertical_tiles: Vec<Tile>, + pub horizontal_tiles: Vec<Tile>, +} + +#[derive(Debug, Clone, Copy)] +pub enum TileOrientation { + Horizontal, + Vertical, +} + +fn parse_row( + y: u32, + img: &(Vec<u8>, u32, u32), + manifest: &TilesetManifest, + orientation: TileOrientation, +) -> Result<Vec<Tile>, String> { + let ey = match orientation { + TileOrientation::Horizontal => y + manifest.dimensions.short, + TileOrientation::Vertical => y + manifest.dimensions.long, + }; + let mut tiles = Vec::new(); + let mut x = 1; + loop { + if x >= img.1 { + break; + } + let left_border = img.0[(x - 1 + y * img.1) as usize]; + // 2 = border color + if left_border != 2 { + break; + } + + let mut tile_pixels = Vec::new(); + let ex = match orientation { + TileOrientation::Horizontal => x + manifest.dimensions.long, + TileOrientation::Vertical => x + manifest.dimensions.short, + }; + + for py in y..ey { + for px in x..ex { + let idx = img.0[(px + py * img.1) as usize]; + let palette = manifest.palette.get(&idx); + + match palette { + Some(pixel_type) => tile_pixels.push(*pixel_type), + None => { + return Err(format!( + "Pixel ({px},{py}) resolved to idx {idx} which isn't in the palette" + )); + } + } + } + } + + debug_assert!( + tile_pixels.len() == (manifest.dimensions.long * manifest.dimensions.short) as usize + ); + + tiles.push(Tile { + pixels: tile_pixels, + }); + + x = ex + 3; + } + Ok(tiles) +} + +fn parse_tiles( + img: &(Vec<u8>, u32, u32), + manifest: &TilesetManifest, +) -> Result<(Vec<Tile>, Vec<Tile>), String> { + let mut horizontal_tiles = Vec::new(); + let mut vertical_tiles = Vec::new(); + let mut y = 0; + loop { + if y >= img.2 { + break; + } + + let edge = img.0[((y) * img.1) as usize] == 2; + if !edge { + y += 1; + continue; + } + + let orientation = if img.0[((y + manifest.dimensions.short + 2) * img.1) as usize] == 0 { + TileOrientation::Horizontal + } else { + TileOrientation::Vertical + }; + + let parse_result = parse_row(y + 1, img, manifest, orientation); + match parse_result { + Ok(mut tiles) => match orientation { + TileOrientation::Horizontal => { + println!("Parsed horizontal row {y}. {n} entries", n = tiles.len()); + horizontal_tiles.append(&mut tiles); + y += manifest.dimensions.short + 3; + } + TileOrientation::Vertical => { + println!("Parsed vertical row {y}. {n} entries", n = tiles.len()); + vertical_tiles.append(&mut tiles); + y += manifest.dimensions.long + 3; + } + }, + Err(e) => { + return Err(format!( + "Failed to parse row {} ({:#?}): {e}", + y + 1, + orientation + )); + } + } + } + + Ok((horizontal_tiles, vertical_tiles)) +} + +pub fn load_tileset(path: &str) -> Tileset { + let img_result = load_img(&Path::join(Path::new(path), Path::new("img.png"))); + match img_result { + Ok(img_result) => { + let manifest_result = + load_manifest(&Path::join(Path::new(path), Path::new("manifest.toml"))); + match manifest_result { + Ok(manifest) => { + debug_assert!(manifest.dimensions.long == manifest.dimensions.short * 2); + let (horizontal_tiles, vertical_tiles) = + parse_tiles(&img_result, &manifest).unwrap(); + + Tileset { + dimensions: manifest.dimensions, + vertical_tiles, + horizontal_tiles, + } + } + Err(e) => { + panic!("Couldn't load sprite: {}", e) + } + } + } + Err(e) => { + panic!("Couldn't load sprite: {}", e) + } + } +} |
