From cc14331326df7daafac8ec38e68d9c767178cc47 Mon Sep 17 00:00:00 2001 From: szdytom Date: Sat, 2 Aug 2025 17:17:21 +0800 Subject: [PATCH] feat: implement mountain smoothing pass with cellular automata and small mountain removal Signed-off-by: szdytom --- tilemap/include/generation.h | 17 +++++- tilemap/src/generation.cpp | 111 ++++++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/tilemap/include/generation.h b/tilemap/include/generation.h index a25044e..9045bd8 100644 --- a/tilemap/include/generation.h +++ b/tilemap/include/generation.h @@ -31,8 +31,9 @@ struct GenerationConfig { int base_octaves = 3; // Number of octaves for base terrain noise double base_persistence = 0.5; // Persistence for base terrain noise + int mountain_smoothen_iteration_n = 3; std::uint32_t mountain_remove_threshold - = 10; // Threshold for mountain removal + = 10; // Threshold for mountain removal std::uint32_t fill_threshold = 10; // Fill holes smaller than this size }; @@ -192,6 +193,18 @@ private: */ void demountainize(TileMap &tilemap, const std::vector &positions); + /** + * @brief Remove small mountain components to create smoother terrain + * @param tilemap The tilemap to process + */ + void remove_small_mountain(TileMap &tilemap); + + /** + * @brief Smoothen mountains with cellular automata + * @param tilemap The tilemap to process + */ + void smoothen_mountains(TileMap &tilemap, std::uint32_t iteration_id); + public: /** * @brief Construct a mountain smoothing pass @@ -201,7 +214,7 @@ public: SmoothenMountainsPass(const GenerationConfig &config, Xoroshiro128PP rng); /** - * @brief Remove small mountain components to create smoother terrain + * @brief Smoothen mountains in the terrain * @param tilemap The tilemap to process */ void operator()(TileMap &tilemap); diff --git a/tilemap/src/generation.cpp b/tilemap/src/generation.cpp index a34311b..78fa44c 100644 --- a/tilemap/src/generation.cpp +++ b/tilemap/src/generation.cpp @@ -277,6 +277,14 @@ SmoothenMountainsPass::SmoothenMountainsPass( : config_(config), noise_(rng) {} void SmoothenMountainsPass::operator()(TileMap &tilemap) { + for (int i = 1; i <= config_.mountain_smoothen_iteration_n; ++i) { + smoothen_mountains(tilemap, i); + } + + remove_small_mountain(tilemap); +} + +void SmoothenMountainsPass::remove_small_mountain(TileMap &tilemap) { std::uint8_t map_size = tilemap.get_size(); std::vector> visited( map_size * Chunk::size, std::vector(map_size * Chunk::size, false) @@ -338,7 +346,7 @@ void SmoothenMountainsPass::demountainize( std::map type_count; std::set unique_positions; for (auto p : pos) { - auto neighbors = tilemap.get_neighbors(p); + auto neighbors = tilemap.get_neighbors(p, true); unique_positions.insert(neighbors.begin(), neighbors.end()); } @@ -414,6 +422,107 @@ std::uint32_t SmoothenMountainsPass::bfs_component_size( return size; } +void SmoothenMountainsPass::smoothen_mountains( + TileMap &tilemap, std::uint32_t iteration_id +) { + struct CAConf { + int neighbor_count; + int fill_chance = 0; // n / 16 + int remove_chance = 0; // n / 16 + }; + + // Chance to fill or remove a mountain tile repects to the number of + // neighboring mountains (0 - 8) + constexpr CAConf cellularAutomataConfigurations[9] = { + { + .neighbor_count = 0, + .remove_chance = 16, + }, + { + .neighbor_count = 1, + .fill_chance = 1, + .remove_chance = 8, + }, + { + .neighbor_count = 2, + .fill_chance = 1, + .remove_chance = 4, + }, + { + .neighbor_count = 3, + .fill_chance = 2, + .remove_chance = 2, + }, + { + .neighbor_count = 4, + .fill_chance = 5, + .remove_chance = 5, + }, + { + .neighbor_count = 5, + .fill_chance = 6, + .remove_chance = 3, + }, + { + .neighbor_count = 6, + .fill_chance = 7, + .remove_chance = 2, + }, + { + .neighbor_count = 7, + .fill_chance = 8, + .remove_chance = 1, + }, + { + .neighbor_count = 8, + .fill_chance = 16, + } + }; + + for (std::uint8_t chunk_x = 0; chunk_x < tilemap.get_size(); ++chunk_x) { + for (std::uint8_t chunk_y = 0; chunk_y < tilemap.get_size(); + ++chunk_y) { + for (std::uint8_t local_x = 0; local_x < Chunk::size; ++local_x) { + for (std::uint8_t local_y = 0; local_y < Chunk::size; + ++local_y) { + TilePos pos{chunk_x, chunk_y, local_x, local_y}; + auto [global_x, global_y] = pos.to_global(); + auto neighbors = tilemap.get_neighbors(pos, true); + + // Ignore if adjacent to the boundary + if (neighbors.size() < 8) { + continue; + } + + // Count neighboring mountains + int mountain_count = 0; + for (const auto &neighbor : neighbors) { + const Tile &tile = tilemap.get_tile(neighbor); + if (tile.base == BaseTileType::Mountain) { + mountain_count += 1; + } + } + + // Get the configuration for this count + const CAConf &conf + = cellularAutomataConfigurations[mountain_count]; + int rd + = noise_.noise(global_x, global_y, iteration_id) & 0xF; + + Tile &tile = tilemap.get_tile(pos); + if (tile.base == BaseTileType::Mountain + && conf.remove_chance > rd) { + demountainize(tilemap, {pos}); + } else if (tile.base != BaseTileType::Mountain + && conf.fill_chance > rd) { + tile.base = BaseTileType::Mountain; + } + } + } + } + } +} + TerrainGenerator::TerrainGenerator(const GenerationConfig &config) : config_(config), master_rng_(config.seed) {}