From 2335bd23e3b4e09177ff02e236612b78c1730b9c Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Mon, 31 Aug 2026 23:36:08 -0700 Subject: argmax materials --- src/content/world/mines.rs | 40 ++++++++++--- src/proc_gen/herringbone.rs | 4 +- src/proc_gen/mod.rs | 131 ++++++++++++++++++++++++++++++++--------- src/proc_gen/tileset_loader.rs | 92 +++++++++++------------------ 4 files changed, 173 insertions(+), 94 deletions(-) (limited to 'src') diff --git a/src/content/world/mines.rs b/src/content/world/mines.rs index d7e23bc..9e24a37 100644 --- a/src/content/world/mines.rs +++ b/src/content/world/mines.rs @@ -1,7 +1,11 @@ use fastnoise_lite::{FastNoiseLite, FractalType}; use glam::IVec2; -use crate::{content::materials::MaterialId, proc_gen::Biome, sim::cell::Cell}; +use crate::{ + content::materials::MaterialId, + proc_gen::{Biome, InterpolatedPixel, tileset_loader::TilePixelType}, + sim::cell::Cell, +}; const ROUGHNESS: f32 = 0.375; @@ -12,22 +16,40 @@ pub struct MinesBiome { impl MinesBiome { pub fn new() -> Self { let mut edge_roughness = FastNoiseLite::new(); - edge_roughness.octaves = 2; - edge_roughness.fractal_type = FractalType::Ridged; + edge_roughness.set_fractal_type(Some(FractalType::Ridged)); + edge_roughness.set_fractal_octaves(Some(2)); MinesBiome { edge_roughness } } } impl Biome for MinesBiome { - fn cell(&self, solidity: f32, world: IVec2) -> Cell { + fn fragment(&self, pixel: InterpolatedPixel, world: IVec2) -> Cell { let world = world.as_vec2(); - let displacement = ROUGHNESS * self.edge_roughness.get_noise_2d(world.x, world.y); - if solidity + displacement <= 0.0 { - return Cell::void(); + match pixel.interpolated_pixel_index { + 0 => { + if pixel.interpolated_solidity + displacement <= 0.0 { + return Cell::void(); + } + + Cell::from_material(MaterialId::Dirt) + } + 12 => { + if pixel.interpolated_solidity + displacement <= 0.0 { + return Cell::void(); + } + + Cell::from_material(MaterialId::Water) + } + 13 => { + if pixel.interpolated_solidity + displacement <= 0.0 { + return Cell::void(); + } + + Cell::from_material(MaterialId::Wood) + } + _ => Cell::void(), } - - Cell::from_material(MaterialId::Dirt) } } diff --git a/src/proc_gen/herringbone.rs b/src/proc_gen/herringbone.rs index 0a67b0c..a01c010 100644 --- a/src/proc_gen/herringbone.rs +++ b/src/proc_gen/herringbone.rs @@ -3,6 +3,8 @@ use glam::IVec2; use crate::proc_gen::tileset_loader::TileOrientation; +const SEED: u32 = 800; + pub fn split_position(pixel: IVec2, short: i32) -> (IVec2, IVec2, TileOrientation) { let grid = pixel.div_euclid(IVec2::splat(short)); let local = pixel.rem_euclid(IVec2::splat(short)); @@ -31,5 +33,5 @@ fn reduce(h: u32, n: usize) -> usize { } pub fn variant_index(grid: IVec2, variants: usize) -> usize { - reduce(hash32(&grid), variants) + reduce(hash32(&(grid, SEED)), variants) } diff --git a/src/proc_gen/mod.rs b/src/proc_gen/mod.rs index 4a1003a..abdd0b8 100644 --- a/src/proc_gen/mod.rs +++ b/src/proc_gen/mod.rs @@ -1,19 +1,29 @@ pub mod herringbone; pub mod tileset_loader; +use std::{ + cmp::Ordering, + collections::HashMap, + ops::{Add, Mul, Sub}, +}; + +use fxhash::FxHashMap; use glam::IVec2; use crate::{ config::{CHUNK_SIZE, TILESET_SCALING}, proc_gen::{ herringbone::{split_position, variant_index}, - tileset_loader::{Tile, TileOrientation, Tileset, TilesetPixelType, load_tileset}, + tileset_loader::{Tile, TileOrientation, TilePixelType, Tileset, load_tileset}, }, sim::{cell::Cell, cell_manager::chunk::Chunk, entity::EntityDef}, }; #[inline] -fn lerp(a: f32, b: f32, t: f32) -> f32 { +fn lerp(a: T, b: T, t: f32) -> T +where + T: Copy + Add + Sub + Mul, +{ a + (b - a) * t } @@ -22,11 +32,16 @@ const PIXELS_NEIGHBOURHOOD_SIZE: i32 = CHUNK_PIXELS + 2; // 2d slice of solidity; chunk + 1 margin struct BiomeChunkContext { - instantaneous_solidity: [f32; (PIXELS_NEIGHBOURHOOD_SIZE * PIXELS_NEIGHBOURHOOD_SIZE) as usize], + pixels: [TilePixelType; (PIXELS_NEIGHBOURHOOD_SIZE * PIXELS_NEIGHBOURHOOD_SIZE) as usize], +} + +pub struct InterpolatedPixel { + pub interpolated_pixel_index: u8, + pub interpolated_solidity: f32, } impl BiomeChunkContext { - fn lerped_solidity_at(&self, local: IVec2) -> f32 { + fn interpolated_at(&self, local: IVec2, tileset: &Tileset) -> InterpolatedPixel { // pixel coord centre of cell coord local let pixel_position = (local.as_vec2() + 0.5) / TILESET_SCALING as f32 + 0.5; let pixel = pixel_position.floor(); @@ -34,22 +49,86 @@ impl BiomeChunkContext { let frac = pixel_position - pixel; let pixel = pixel.as_ivec2(); - let at = |dx, dy| { - self.instantaneous_solidity - [((pixel.x + dx) + (pixel.y + dy) * PIXELS_NEIGHBOURHOOD_SIZE) as usize] + let instantaneous_pixel_at = |dx, dy| { + self.pixels[((pixel.x + dx) + (pixel.y + dy) * PIXELS_NEIGHBOURHOOD_SIZE) as usize] + }; + + let instantaneous_idx_at = |dx, dy| match instantaneous_pixel_at(dx, dy) { + TilePixelType::Void => 255, + TilePixelType::Terrain(_) => 0, + TilePixelType::Custom(i) => i, + }; + + let instantaneous_solidity_at = |dx, dy| match instantaneous_pixel_at(dx, dy) { + TilePixelType::Void => 0.0, + TilePixelType::Terrain(o) => o, + TilePixelType::Custom(i) => { + tileset.manifest.palette.get(&i).map_or(0.0, |p| p.solidity) + } }; // linearly interpolate between the solidity of the horizontal cells above and below according to the fractional x component - let top = lerp(at(0, 0), at(1, 0), frac.x); - let bottom = lerp(at(0, 1), at(1, 1), frac.x); + let s_top = lerp( + instantaneous_solidity_at(0, 0), + instantaneous_solidity_at(1, 0), + frac.x, + ); + let s_bottom = lerp( + instantaneous_solidity_at(0, 1), + instantaneous_solidity_at(1, 1), + frac.x, + ); // and then interpolate between those according to the y component - lerp(top, bottom, frac.y) - 0.5 + let interpolated_solidity = lerp(s_top, s_bottom, frac.y) - 0.5; + + // same for the pixel + // TODO don't allocate per-cell + let mut idxes: Vec = Vec::new(); + let tl = instantaneous_idx_at(0, 0); + if tl != 255 && !idxes.contains(&tl) { + idxes.push(tl); + } + let tr = instantaneous_idx_at(1, 0); + if tr != 255 && !idxes.contains(&tr) { + idxes.push(tr); + } + let bl = instantaneous_idx_at(0, 1); + if bl != 255 && !idxes.contains(&bl) { + idxes.push(bl); + } + let br = instantaneous_idx_at(1, 1); + if br != 255 && !idxes.contains(&br) { + idxes.push(br); + } + + let idx_strengths = idxes.iter().map(|&i| { + let i_top = lerp( + if tl == i { 1.0 } else { 0.0 }, + if tr == i { 1.0 } else { 0.0 }, + frac.x, + ); + let i_bottom = lerp( + if bl == i { 1.0 } else { 0.0 }, + if br == i { 1.0 } else { 0.0 }, + frac.x, + ); + + let interpolated_strength = lerp(i_top, i_bottom, frac.y); + (i, interpolated_strength) + }); + + InterpolatedPixel { + interpolated_pixel_index: idx_strengths + .max_by(|&(_, s1), &(_, s2)| s1.total_cmp(&s2)) + .map_or(255, |(i, _)| i), + interpolated_solidity, + } } } pub trait Biome { - fn cell(&self, solidity: f32, world: IVec2) -> Cell; + fn fragment(&self, pixel: InterpolatedPixel, world: IVec2) -> Cell; fn derive_entities_from_tile(&self, _tile: &Tile) -> Vec { Vec::new() @@ -69,9 +148,9 @@ impl TilesetWorldGenerator { } } - fn pixel_at(&self, pixel: IVec2) -> TilesetPixelType { + fn pixel_at(&self, pixel: IVec2) -> TilePixelType { let (grid, local, orientation) = - split_position(pixel, self.tileset.dimensions.short as i32); + split_position(pixel, self.tileset.manifest.dimensions.short as i32); let tiles = match orientation { TileOrientation::Horizontal => &self.tileset.horizontal_tiles, @@ -81,39 +160,37 @@ impl TilesetWorldGenerator { tiles[variant_index(grid, tiles.len())].pixel_at(local) } - fn structure_around(&self, chunk_position: IVec2) -> BiomeChunkContext { + fn get_biome_chunk_context(&self, chunk_position: IVec2) -> BiomeChunkContext { let origin = chunk_position * CHUNK_PIXELS - 1; - let mut occupancy = [0.0; (PIXELS_NEIGHBOURHOOD_SIZE * PIXELS_NEIGHBOURHOOD_SIZE) as usize]; + let mut pixels = + [TilePixelType::Void; (PIXELS_NEIGHBOURHOOD_SIZE * PIXELS_NEIGHBOURHOOD_SIZE) as usize]; for y in 0..PIXELS_NEIGHBOURHOOD_SIZE { for x in 0..PIXELS_NEIGHBOURHOOD_SIZE { - occupancy[(x + y * PIXELS_NEIGHBOURHOOD_SIZE) as usize] = - match self.pixel_at(origin + IVec2::new(x, y)) { - TilesetPixelType::Void => 0.0, - TilesetPixelType::Terrain => 1.0, - }; + pixels[(x + y * PIXELS_NEIGHBOURHOOD_SIZE) as usize] = + self.pixel_at(origin + IVec2::new(x, y)); } } - BiomeChunkContext { - instantaneous_solidity: occupancy, - } + BiomeChunkContext { pixels } } pub fn generate_chunk(&self, chunk_position: IVec2) -> Chunk { let mut chunk = Chunk::void(); - let structure = self.structure_around(chunk_position); + let biome_chunk_context = self.get_biome_chunk_context(chunk_position); let chunk_min = chunk_position * CHUNK_SIZE; for y in 0..CHUNK_SIZE { for x in 0..CHUNK_SIZE { let local = IVec2::new(x, y); - chunk.cells[(x + y * CHUNK_SIZE) as usize] = self - .biome - .cell(structure.lerped_solidity_at(local), chunk_min + local); + chunk.cells[(x + y * CHUNK_SIZE) as usize] = self.biome.fragment( + biome_chunk_context.interpolated_at(local, &self.tileset), + chunk_min + local, + ); } } + chunk.sleeping = false; chunk.mark_collider_dirty(0); chunk diff --git a/src/proc_gen/tileset_loader.rs b/src/proc_gen/tileset_loader.rs index e0d9d18..24bb053 100644 --- a/src/proc_gen/tileset_loader.rs +++ b/src/proc_gen/tileset_loader.rs @@ -2,8 +2,6 @@ use glam::IVec2; use serde::Deserialize; use std::{collections::HashMap, error::Error, fs::File, io::BufReader, path::Path}; -use crate::sim::lib::marching_squares::Marchable; - fn load_img(path: &Path) -> Result<(Vec, u32, u32), Box> { let mut decoder = png::Decoder::new(BufReader::new(File::open(path)?)); decoder.set_transformations(png::Transformations::IDENTITY); @@ -35,16 +33,22 @@ pub struct TileDimensions { pub long: u32, } -#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -pub enum TilesetPixelType { - Void = 0, - Terrain, +#[derive(Deserialize, Clone, Copy, Debug)] +pub enum TilePixelType { + Void, + Terrain(f32), + Custom(u8), +} + +#[derive(Deserialize, Debug)] +pub struct PaletteEntry { + pub solidity: f32, } #[derive(Deserialize, Debug)] -struct TilesetManifest { - dimensions: TileDimensions, - palette: HashMap, +pub struct TilesetManifest { + pub dimensions: TileDimensions, + pub palette: HashMap, } fn load_manifest(path: &Path) -> Result> { @@ -55,39 +59,7 @@ fn load_manifest(path: &Path) -> Result> { pub struct Tile { pub orientation: TileOrientation, pub dimensions: TileDimensions, - pub pixels: Vec, -} - -// TODO might want to use newtypes to hoist to biome -impl Marchable for Tile { - fn occupied(&self, pos: IVec2) -> bool { - // TODO this will break for tileset dimensions other than 22x44 - if pos.x < 0 || pos.x >= self.size().x || pos.y < 0 || pos.y >= self.size().y { - match self.orientation { - TileOrientation::Horizontal => { - let a = (pos.x == -1 || pos.x == (self.dimensions.long as i32)) - && pos.y >= 8 - && pos.y <= 13; - let b = (pos.y == -1 || pos.y == (self.dimensions.short as i32)) - && ((pos.x >= 8 && pos.x <= 13) || (pos.x >= 30 && pos.x <= 35)); - !(a || b) - } - TileOrientation::Vertical => { - let a = (pos.y == -1 || pos.y == (self.dimensions.long as i32)) - && pos.x >= 8 - && pos.x <= 13; - let b = (pos.x == -1 || pos.x == (self.dimensions.short as i32)) - && ((pos.y >= 8 && pos.y <= 13) || (pos.y >= 30 && pos.y <= 35)); - !(a || b) - } - } - } else { - self.pixel_at(pos) == TilesetPixelType::Terrain - } - } - fn marchable_size(&self) -> IVec2 { - self.size() - } + pub pixels: Vec, } impl Tile { @@ -101,14 +73,14 @@ impl Tile { } #[inline] - pub fn pixel_at(&self, position: IVec2) -> TilesetPixelType { + pub fn pixel_at(&self, position: IVec2) -> TilePixelType { self.pixels[(position.x + position.y * self.size().x) as usize] } } #[derive(Debug)] pub struct Tileset { - pub dimensions: TileDimensions, + pub manifest: TilesetManifest, pub vertical_tiles: Vec, pub horizontal_tiles: Vec, } @@ -136,8 +108,7 @@ fn parse_row( break; } let left_border = img.0[(x - 1 + y * img.1) as usize]; - // 2 = border color - if left_border != 2 { + if left_border != 9 { break; } @@ -150,16 +121,23 @@ fn parse_row( 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" - )); - } - } + let pixel_type = match idx { + 0 => TilePixelType::Void, + 1 => TilePixelType::Terrain(0.125), + 2 => TilePixelType::Terrain(0.25), + 3 => TilePixelType::Terrain(0.375), + 4 => TilePixelType::Terrain(0.5), + 5 => TilePixelType::Terrain(0.625), + 6 => TilePixelType::Terrain(0.75), + 7 => TilePixelType::Terrain(0.875), + 8 => TilePixelType::Terrain(1.0), + // 9 = border + 9 => unreachable!(), + _ => TilePixelType::Custom(idx), + }; + + tile_pixels.push(pixel_type); } } @@ -190,7 +168,7 @@ fn parse_tiles( break; } - let edge = img.0[((y) * img.1) as usize] == 2; + let edge = img.0[((y) * img.1) as usize] == 9; if !edge { y += 1; continue; @@ -242,7 +220,7 @@ pub fn load_tileset(path: &str) -> Tileset { parse_tiles(&img_result, &manifest).unwrap(); Tileset { - dimensions: manifest.dimensions, + manifest, vertical_tiles, horizontal_tiles, } -- cgit v1.3.1