From c5c62000a26038b3a9fae818a5aab89ca2f1069e Mon Sep 17 00:00:00 2001 From: szdytom Date: Sat, 2 Aug 2025 16:41:50 +0800 Subject: [PATCH] feat: add SmoothenMountainsPass for terrain smoothing and enhance TileMap with boundary and neighbor methods Signed-off-by: szdytom --- tilemap/docs/api.md | 102 +++++++++++++++-- tilemap/include/chunk.h | 45 +++++++- tilemap/include/generation.h | 58 ++++++++-- tilemap/include/noise.h | 36 ++++++ tilemap/include/tilemap.h | 18 +++ tilemap/src/chunk.cpp | 29 +++++ tilemap/src/generation.cpp | 214 +++++++++++++++++++++++++---------- tilemap/src/noise.cpp | 36 ++++++ tilemap/src/tilemap.cpp | 36 ++++++ 9 files changed, 489 insertions(+), 85 deletions(-) diff --git a/tilemap/docs/api.md b/tilemap/docs/api.md index f069eee..876add6 100644 --- a/tilemap/docs/api.md +++ b/tilemap/docs/api.md @@ -29,12 +29,19 @@ public: Tile& get_tile(TilePos pos); const Tile& get_tile(TilePos pos) const; void set_tile(TilePos pos, const Tile& tile); + + bool is_at_boundary(TilePos pos) const; + std::vector get_neighbors(TilePos pos, bool chebyshev = false) const; }; ``` **Constructor Parameters:** - `size`: Number of chunks per side (max 100), creating an n×n grid +**New Methods:** +- `is_at_boundary()`: Checks if a tile position is at the map boundary +- `get_neighbors()`: Returns neighboring tile positions with optional Chebyshev distance support + ### Chunk Each chunk contains 64×64 tiles and sub-chunk biome information. @@ -42,13 +49,13 @@ Each chunk contains 64×64 tiles and sub-chunk biome information. ```cpp struct Chunk { static constexpr uint8_t size = 64; // Tiles per side - static constexpr uint8_t subchunk_size = /*default value*/; // Tiles per sub-chunk side + static constexpr uint8_t subchunk_size = 4; // Tiles per sub-chunk side static constexpr uint8_t subchunk_count = size / subchunk_size; // Sub-chunks per side Tile tiles[size][size]; // 64x64 tile grid BiomeType biome[subchunk_count][subchunk_count]; // Sub-chunk biomes - // Get biome for a specific sub-chunk position + // Methods for biome access BiomeType& get_biome(SubChunkPos pos); const BiomeType& get_biome(SubChunkPos pos) const; }; @@ -79,7 +86,7 @@ struct Tile { ### TilePos -Position structure for locating tiles within the map. +Position structure for locating tiles within the map with enhanced coordinate conversion support. ```cpp struct TilePos { @@ -87,7 +94,14 @@ struct TilePos { uint8_t chunk_y; // Chunk Y coordinate uint8_t local_x; // Tile X within chunk (0-63) uint8_t local_y; // Tile Y within chunk (0-63) + + // Coordinate conversion methods + std::pair to_global() const; + static TilePos from_global(std::uint16_t global_x, std::uint16_t global_y); }; + +// Three-way comparison operator for ordering +std::strong_ordering operator<=>(const TilePos& lhs, const TilePos& rhs); ``` ### SubChunkPos @@ -134,8 +148,11 @@ struct GenerationConfig { int base_octaves = 3; // Number of octaves for base terrain noise double base_persistence = 0.5; // Persistence for base terrain noise + // Mountain smoothing parameters + std::uint32_t mountain_remove_threshold = 10; // Remove mountain components smaller than this size + // Hole filling parameters - std::uint32_t fill_threshold = 16; // Fill holes smaller than this size + std::uint32_t fill_threshold = 10; // Fill holes smaller than this size }; ``` @@ -148,10 +165,11 @@ struct GenerationConfig { - `humidity_scale`: Controls the scale/frequency of humidity variation across the map - `humidity_octaves`: Number of noise octaves for humidity - `humidity_persistence`: How much each octave contributes to humidity noise (0.0-1.0) -- `base_scale`: Controls the scale/frequency of base terrain height variation +- `base_scale`: Controls the scale/frequency of base terrain variation across the map - `base_octaves`: Number of noise octaves for base terrain - `base_persistence`: How much each octave contributes to base terrain noise (0.0-1.0) -- `fill_threshold`: Maximum size of connected components to fill with mountains (hole filling) +- `mountain_remove_threshold`: Maximum size of mountain components to remove for terrain smoothing +- `fill_threshold`: Maximum size of holes to fill with mountains ### Generation Passes @@ -241,6 +259,33 @@ private: - **Mountain-as-Impassable**: Treats mountains as impassable terrain for connectivity - **Hole Filling**: Converts small isolated areas to mountains for cleaner terrain +#### SmoothenMountainsPass + +Removes small mountain components to create smoother terrain using BFS and replacement strategies. + +```cpp +class SmoothenMountainsPass { +public: + SmoothenMountainsPass(const GenerationConfig& config, Xoroshiro128PP rng); + void operator()(TileMap& tilemap); + +private: + std::uint32_t bfs_component_size( + TileMap& tilemap, TilePos start_pos, + std::vector>& visited, + std::vector& positions + ); + void demountainize(TileMap& tilemap, const std::vector& positions); +}; +``` + +**Key Features:** +- **Mountain Component Detection**: Uses BFS to find connected mountain regions +- **Size-based Removal**: Removes mountain components smaller than `mountain_remove_threshold` +- **Boundary Preservation**: Preserves mountain components that touch the map boundary +- **Intelligent Replacement**: Replaces mountains with terrain types based on neighboring tiles +- **Smooth Terrain**: Creates more natural-looking terrain without isolated mountain clusters + ### TerrainGenerator Main orchestrator class that manages the generation process using multiple passes. @@ -254,10 +299,23 @@ public: private: void biome_pass(TileMap& tilemap); void base_tile_type_pass(TileMap& tilemap); + void smoothen_mountains_pass(TileMap& tilemap); void hole_fill_pass(TileMap& tilemap); }; ``` +**Generation Order:** +1. **Biome Pass**: Generates climate-based biome data for sub-chunks +2. **Base Tile Type Pass**: Generates base terrain types based on biomes +3. **Smoothen Mountains Pass**: Removes small mountain components for smoother terrain +4. **Hole Fill Pass**: Fills small holes in the terrain + +**Key Features:** +- **Multi-pass Architecture**: Separates generation concerns for better control +- **RNG Management**: Uses independent RNGs for each pass with proper seeding +- **Deterministic Results**: Same seed produces identical terrain across runs +- **Configurable**: All passes use parameters from GenerationConfig + **Generation Flow:** 1. **Biome Pass**: Generate climate data and assign biomes to sub-chunks 2. **Base Tile Type Pass**: Generate base terrain types based on biomes and noise @@ -321,6 +379,24 @@ public: ## Noise System +### DiscreteRandomNoise + +Discrete random noise generator using Xoroshiro128++ for terrain replacement operations. + +```cpp +class DiscreteRandomNoise { +public: + explicit DiscreteRandomNoise(Xoroshiro128PP rng); + std::uint64_t noise(std::uint32_t x, std::uint32_t y, std::uint32_t z = 0) const; +}; +``` + +**Key Features:** +- **Discrete Output**: Produces integer values for discrete selections +- **High Quality**: Based on Xoroshiro128++ random number generation +- **3D Support**: Supports optional Z coordinate for 3D noise +- **Fast**: Optimized for performance in terrain processing + ### PerlinNoise Standard Perlin noise implementation using Xoroshiro128++ for procedural generation. @@ -428,8 +504,11 @@ config.base_scale = 0.08; config.base_octaves = 3; config.base_persistence = 0.5; +// Mountain smoothing settings +config.mountain_remove_threshold = 10; // Remove mountain components smaller than 10 tiles + // Hole filling settings -config.fill_threshold = 16; // Fill holes smaller than 16 tiles +config.fill_threshold = 10; // Fill holes smaller than 10 tiles // Generate terrain istd::map_generate(tilemap, config); @@ -582,6 +661,15 @@ The new pass-based system provides: 3. **Reproducible Results**: Same seed produces identical results across passes 4. **Extensibility**: Easy to add new passes or modify existing ones 5. **Performance**: Efficient memory access patterns and reduced redundant calculations +6. **Terrain Quality**: SmoothenMountainsPass creates more natural-looking terrain + +### Recent Improvements + +**Mountain Smoothing**: The new `SmoothenMountainsPass` removes small isolated mountain components to create more natural terrain formations. Small mountain clusters that don't connect to the boundary are replaced with terrain types based on their neighboring areas. + +**Enhanced TileMap**: Added utility methods for boundary detection and neighbor finding, supporting both Manhattan and Chebyshev distance calculations. + +**Improved Noise**: Added `DiscreteRandomNoise` for high-quality discrete value generation used in terrain replacement operations. ## Thread Safety diff --git a/tilemap/include/chunk.h b/tilemap/include/chunk.h index d3fd03c..ffef265 100644 --- a/tilemap/include/chunk.h +++ b/tilemap/include/chunk.h @@ -1,6 +1,7 @@ #ifndef ISTD_TILEMAP_CHUNK_H #define ISTD_TILEMAP_CHUNK_H #include "tile.h" +#include #include namespace istd { @@ -8,7 +9,9 @@ namespace istd { // Forward declaration enum class BiomeType : std::uint8_t; -// Position within a chunk's sub-chunk grid +/** + * @brief Position within a chunk's sub-chunk grid + */ struct SubChunkPos { std::uint8_t sub_x; std::uint8_t sub_y; @@ -23,8 +26,30 @@ struct TilePos { uint8_t chunk_y; uint8_t local_x; uint8_t local_y; + + /** + * @brief Convert TilePos to global coordinates + * @return Pair of global X and Y coordinates + */ + std::pair to_global() const; + + /** + * @brief Construct a TilePos from global coordinates + * @param global_x Global X coordinate + * @param global_y Global Y coordinate + * @return TilePos corresponding to the global coordinates + */ + static TilePos from_global(std::uint16_t global_x, std::uint16_t global_y); }; +/** + * @brief Three-way comparison operator for TilePos + * @param lhs Left-hand side TilePos + * @param rhs Right-hand side TilePos + * @return Strong ordering comparison result + */ +std::strong_ordering operator<=>(const TilePos &lhs, const TilePos &rhs); + struct Chunk { // Size of a chunk in tiles (64 x 64) static constexpr uint8_t size = 64; @@ -41,18 +66,30 @@ struct Chunk { // array of biomes for sub-chunks BiomeType biome[subchunk_count][subchunk_count]; - // Get biome for a specific sub-chunk position + /** + * @brief Get biome for a specific sub-chunk position + * @param pos Sub-chunk position + * @return Reference to biome type + */ BiomeType &get_biome(SubChunkPos pos) { return biome[pos.sub_x][pos.sub_y]; } - // Get biome for a specific sub-chunk position (const version) + /** + * @brief Get biome for a specific sub-chunk position (const version) + * @param pos Sub-chunk position + * @return Const reference to biome type + */ const BiomeType &get_biome(SubChunkPos pos) const { return biome[pos.sub_x][pos.sub_y]; } }; -// Get the starting tile coordinates for a sub-chunk +/** + * @brief Get the starting tile coordinates for a sub-chunk + * @param pos Sub-chunk position + * @return Pair of starting tile coordinates (x, y) + */ std::pair subchunk_to_tile_start(SubChunkPos pos); } // namespace istd diff --git a/tilemap/include/generation.h b/tilemap/include/generation.h index 08a4621..a25044e 100644 --- a/tilemap/include/generation.h +++ b/tilemap/include/generation.h @@ -12,6 +12,9 @@ namespace istd { +/** + * @brief Configuration parameters for terrain generation + */ struct GenerationConfig { Seed seed; @@ -28,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 - // Hole filling parameters - std::uint32_t fill_threshold = 16; // Fill holes smaller than this size + std::uint32_t mountain_remove_threshold + = 10; // Threshold for mountain removal + std::uint32_t fill_threshold = 10; // Fill holes smaller than this size }; class BiomeGenerationPass { @@ -161,22 +165,46 @@ private: TileMap &tilemap, TilePos start_pos, std::vector> &visited, std::vector &positions ); +}; + +class SmoothenMountainsPass { +private: + const GenerationConfig &config_; + DiscreteRandomNoise noise_; /** - * @brief Get all valid neighbors of a position - * @param tilemap The tilemap for bounds checking - * @param pos The position to get neighbors for - * @return Vector of valid neighbor positions + * @brief Perform BFS to find connected component size for mountains + * @param tilemap The tilemap to search + * @param start_pos Starting position for BFS + * @param visited 2D array tracking visited tiles + * @param positions Output vector of positions in this component + * @return Size of the connected component */ - std::vector get_neighbors(TileMap &tilemap, TilePos pos) const; + std::uint32_t bfs_component_size( + TileMap &tilemap, TilePos start_pos, + std::vector> &visited, std::vector &positions + ); /** - * @brief Check if a position is at the map boundary - * @param tilemap The tilemap for bounds checking - * @param pos The position to check - * @return True if the position is at the boundary + * @brief Replace mountain tiles with terrain types from neighboring areas + * @param tilemap The tilemap to modify + * @param positions Vector of mountain positions to replace */ - bool is_at_boundary(TileMap &tilemap, TilePos pos) const; + void demountainize(TileMap &tilemap, const std::vector &positions); + +public: + /** + * @brief Construct a mountain smoothing pass + * @param config Generation configuration parameters + * @param rng Random number generator for terrain replacement + */ + SmoothenMountainsPass(const GenerationConfig &config, Xoroshiro128PP rng); + + /** + * @brief Remove small mountain components to create smoother terrain + * @param tilemap The tilemap to process + */ + void operator()(TileMap &tilemap); }; // Terrain generator class that manages the generation process @@ -211,6 +239,12 @@ private: */ void base_tile_type_pass(TileMap &tilemap); + /** + * @brief Smoothen mountains in the terrain + * @param tilemap The tilemap to process + */ + void smoothen_mountains_pass(TileMap &tilemap); + /** * @brief Fill small holes in the terrain * @param tilemap The tilemap to process diff --git a/tilemap/include/noise.h b/tilemap/include/noise.h index c2a8ae9..856e500 100644 --- a/tilemap/include/noise.h +++ b/tilemap/include/noise.h @@ -2,11 +2,47 @@ #define ISTD_TILEMAP_NOISE_H #include "xoroshiro.h" +#include #include #include namespace istd { +/** + * @brief Discrete random noise generator for terrain replacement operations + * + * Provides high-quality discrete random values based on Xoroshiro128++ RNG. + * Used for selecting terrain types during mountain smoothing operations. + */ +class DiscreteRandomNoise { +private: + std::uint64_t mask; + std::array permutation_; + + std::uint8_t perm(int x) const; + std::uint32_t map(std::uint32_t x) const; + +public: + /** + * @brief Construct a DiscreteRandomNoise generator with the given seed + * @param rng Random number generator for noise + */ + explicit DiscreteRandomNoise(Xoroshiro128PP rng); + + DiscreteRandomNoise() = default; + + /** + * @brief Generate a discrete random value at the given coordinates + * @param x X coordinate + * @param y Y coordinate + * @param z Z coordinate (optional) + * @return Discrete random value between 0 and 255 + */ + std::uint64_t noise( + std::uint32_t x, std::uint32_t y, std::uint32_t z = 0 + ) const; +}; + class PerlinNoise { private: std::vector permutation_; diff --git a/tilemap/include/tilemap.h b/tilemap/include/tilemap.h index e90e5b0..ef262dd 100644 --- a/tilemap/include/tilemap.h +++ b/tilemap/include/tilemap.h @@ -47,6 +47,24 @@ public: * @param tile The tile to set */ void set_tile(TilePos pos, const Tile &tile); + + /** + * @brief Check if a position is at the map boundary + * @param pos The position to check + * @return True if the position is at the boundary + */ + bool is_at_boundary(TilePos pos) const; + + /** + * @brief Get all valid neighbors of a position + * @param pos The position to get neighbors for + * @param chebyshev If true, use Chebyshev distance (8-connected), otherwise + * Manhattan distance (4-connected) + * @return Vector of valid neighbor positions + */ + std::vector get_neighbors( + TilePos pos, bool chebyshev = false + ) const; }; } // namespace istd diff --git a/tilemap/src/chunk.cpp b/tilemap/src/chunk.cpp index bdb73f8..e5eb6c9 100644 --- a/tilemap/src/chunk.cpp +++ b/tilemap/src/chunk.cpp @@ -2,9 +2,38 @@ namespace istd { +std::strong_ordering operator<=>(const TilePos &lhs, const TilePos &rhs) { + if (lhs.chunk_x != rhs.chunk_x) { + return lhs.chunk_x <=> rhs.chunk_x; + } + + if (lhs.chunk_y != rhs.chunk_y) { + return lhs.chunk_y <=> rhs.chunk_y; + } + + if (lhs.local_x != rhs.local_x) { + return lhs.local_x <=> rhs.local_x; + } + + return lhs.local_y <=> rhs.local_y; +} + std::pair subchunk_to_tile_start(SubChunkPos pos) { // Convert sub-chunk position to tile start coordinates return {pos.sub_x * Chunk::subchunk_size, pos.sub_y * Chunk::subchunk_size}; } +std::pair TilePos::to_global() const { + return {chunk_x * Chunk::size + local_x, chunk_y * Chunk::size + local_y}; +} + +TilePos TilePos::from_global(std::uint16_t global_x, std::uint16_t global_y) { + return { + static_cast(global_x / Chunk::size), + static_cast(global_y / Chunk::size), + static_cast(global_x % Chunk::size), + static_cast(global_y % Chunk::size) + }; +} + } // namespace istd diff --git a/tilemap/src/generation.cpp b/tilemap/src/generation.cpp index 6de2ec8..a34311b 100644 --- a/tilemap/src/generation.cpp +++ b/tilemap/src/generation.cpp @@ -1,7 +1,9 @@ #include "generation.h" #include "biome.h" #include +#include #include +#include #include namespace istd { @@ -182,8 +184,7 @@ void HoleFillPass::operator()(TileMap &tilemap) { for (std::uint8_t local_y = 0; local_y < Chunk::size; ++local_y) { TilePos pos{chunk_x, chunk_y, local_x, local_y}; - std::uint32_t global_x = chunk_x * Chunk::size + local_x; - std::uint32_t global_y = chunk_y * Chunk::size + local_y; + auto [global_x, global_y] = pos.to_global(); // Skip if already visited if (visited[global_x][global_y]) { @@ -206,8 +207,8 @@ void HoleFillPass::operator()(TileMap &tilemap) { // Check if this component touches the boundary bool touches_boundary = false; - for (const TilePos &component_pos : component_positions) { - if (is_at_boundary(tilemap, component_pos)) { + for (const auto component_pos : component_positions) { + if (tilemap.is_at_boundary(component_pos)) { touches_boundary = true; break; } @@ -215,7 +216,7 @@ void HoleFillPass::operator()(TileMap &tilemap) { // Fill small holes that don't touch the boundary if (!touches_boundary - && component_size < config_.fill_threshold) { + && component_size <= config_.fill_threshold) { for (const TilePos &fill_pos : component_positions) { Tile fill_tile = tilemap.get_tile(fill_pos); fill_tile.base = BaseTileType::Mountain; @@ -239,11 +240,7 @@ std::uint32_t HoleFillPass::bfs_component_size( std::queue queue; queue.push(start_pos); - std::uint8_t map_size = tilemap.get_size(); - std::uint32_t start_global_x - = start_pos.chunk_x * Chunk::size + start_pos.local_x; - std::uint32_t start_global_y - = start_pos.chunk_y * Chunk::size + start_pos.local_y; + auto [start_global_x, start_global_y] = start_pos.to_global(); visited[start_global_x][start_global_y] = true; std::uint32_t size = 0; @@ -256,13 +253,9 @@ std::uint32_t HoleFillPass::bfs_component_size( ++size; // Check all neighbors - std::vector neighbors = get_neighbors(tilemap, current); - for (const TilePos &neighbor : neighbors) { - std::uint32_t neighbor_global_x - = neighbor.chunk_x * Chunk::size + neighbor.local_x; - std::uint32_t neighbor_global_y - = neighbor.chunk_y * Chunk::size + neighbor.local_y; - + std::vector neighbors = tilemap.get_neighbors(current); + for (const auto neighbor : neighbors) { + auto [neighbor_global_x, neighbor_global_y] = neighbor.to_global(); if (visited[neighbor_global_x][neighbor_global_y]) { continue; } @@ -278,57 +271,159 @@ std::uint32_t HoleFillPass::bfs_component_size( return size; } -std::vector HoleFillPass::get_neighbors( - TileMap &tilemap, TilePos pos -) const { - std::vector neighbors; +SmoothenMountainsPass::SmoothenMountainsPass( + const GenerationConfig &config, Xoroshiro128PP rng +) + : config_(config), noise_(rng) {} + +void SmoothenMountainsPass::operator()(TileMap &tilemap) { std::uint8_t map_size = tilemap.get_size(); + std::vector> visited( + map_size * Chunk::size, std::vector(map_size * Chunk::size, false) + ); - // Calculate global coordinates - std::uint32_t global_x = pos.chunk_x * Chunk::size + pos.local_x; - std::uint32_t global_y = pos.chunk_y * Chunk::size + pos.local_y; - std::uint32_t max_global = map_size * Chunk::size; + for (std::uint8_t chunk_x = 0; chunk_x < map_size; ++chunk_x) { + for (std::uint8_t chunk_y = 0; chunk_y < map_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(); - // Four cardinal directions - const int dx[] = {-1, 1, 0, 0}; - const int dy[] = {0, 0, -1, 1}; + // Skip if already visited + if (visited[global_x][global_y]) { + continue; + } - for (int i = 0; i < 4; ++i) { - int new_global_x = static_cast(global_x) + dx[i]; - int new_global_y = static_cast(global_y) + dy[i]; + const Tile &tile = tilemap.get_tile(pos); + if (tile.base != BaseTileType::Mountain) { + visited[global_x][global_y] = true; + continue; + } - // Check bounds - if (new_global_x >= 0 && new_global_x < static_cast(max_global) - && new_global_y >= 0 - && new_global_y < static_cast(max_global)) { - // Convert back to chunk and local coordinates - std::uint8_t new_chunk_x = new_global_x / Chunk::size; - std::uint8_t new_chunk_y = new_global_y / Chunk::size; - std::uint8_t new_local_x = new_global_x % Chunk::size; - std::uint8_t new_local_y = new_global_y % Chunk::size; + // Find connected component of mountains + std::vector component_positions; + std::uint32_t component_size = bfs_component_size( + tilemap, pos, visited, component_positions + ); - neighbors.push_back( - {new_chunk_x, new_chunk_y, new_local_x, new_local_y} - ); + // If the component touches the boundary, skip it + bool touches_boundary = false; + for (auto component_pos : component_positions) { + if (tilemap.is_at_boundary(component_pos)) { + touches_boundary = true; + break; + } + } + + // Skip if it touches the boundary + if (touches_boundary) { + continue; + } + + // If the component is too small, smooth it out + if (component_size <= config_.mountain_remove_threshold) { + demountainize(tilemap, component_positions); + } + } + } + } + } +} + +void SmoothenMountainsPass::demountainize( + TileMap &tilemap, const std::vector &pos +) { + // Step 1: Look around the mountain to see what should replace it + std::map type_count; + std::set unique_positions; + for (auto p : pos) { + auto neighbors = tilemap.get_neighbors(p); + unique_positions.insert(neighbors.begin(), neighbors.end()); + } + + for (auto p : unique_positions) { + const Tile &tile = tilemap.get_tile(p); + if (tile.base != BaseTileType::Mountain) { + type_count[tile.base]++; } } - return neighbors; + int total_count = 0; + for (const auto &[type, count] : type_count) { + total_count += count; + } + + if (total_count == 0) { + std::unreachable(); + } + + // Step 2: Replace each mountain tile with a random type based on the counts + for (const auto &p : pos) { + Tile tile = tilemap.get_tile(p); + auto [global_x, global_y] = p.to_global(); + auto sample = noise_.noise(global_x, global_y); + int index = sample % total_count; // Not perfectly uniform, but works + // for small counts + for (const auto [type, count] : type_count) { + if (index < count) { + tile.base = type; + break; + } + index -= count; + } + tilemap.set_tile(p, tile); + } } -bool HoleFillPass::is_at_boundary(TileMap &tilemap, TilePos pos) const { - std::uint8_t map_size = tilemap.get_size(); - std::uint32_t global_x = pos.chunk_x * Chunk::size + pos.local_x; - std::uint32_t global_y = pos.chunk_y * Chunk::size + pos.local_y; - std::uint32_t max_global = map_size * Chunk::size - 1; +std::uint32_t SmoothenMountainsPass::bfs_component_size( + TileMap &tilemap, TilePos start_pos, + std::vector> &visited, std::vector &positions +) { + std::queue queue; + queue.push(start_pos); - return global_x == 0 || global_x == max_global || global_y == 0 - || global_y == max_global; + auto [start_global_x, start_global_y] = start_pos.to_global(); + visited[start_global_x][start_global_y] = true; + + std::uint32_t size = 0; + positions.clear(); + + while (!queue.empty()) { + TilePos current = queue.front(); + queue.pop(); + positions.push_back(current); + ++size; + + // Check all neighbors + std::vector neighbors = tilemap.get_neighbors(current, true); + for (const auto neighbor : neighbors) { + auto [neighbor_global_x, neighbor_global_y] = neighbor.to_global(); + if (visited[neighbor_global_x][neighbor_global_y]) { + continue; + } + + const Tile &neighbor_tile = tilemap.get_tile(neighbor); + if (neighbor_tile.base == BaseTileType::Mountain) { + visited[neighbor_global_x][neighbor_global_y] = true; + queue.push(neighbor); + } + } + } + + return size; } TerrainGenerator::TerrainGenerator(const GenerationConfig &config) : config_(config), master_rng_(config.seed) {} +void TerrainGenerator::operator()(TileMap &tilemap) { + biome_pass(tilemap); + base_tile_type_pass(tilemap); + smoothen_mountains_pass(tilemap); + hole_fill_pass(tilemap); +} + void TerrainGenerator::biome_pass(TileMap &tilemap) { // Create two RNGs for temperature and humidity noise Xoroshiro128PP temp_rng = master_rng_; @@ -340,23 +435,18 @@ void TerrainGenerator::biome_pass(TileMap &tilemap) { biome_pass(tilemap); } -void TerrainGenerator::operator()(TileMap &tilemap) { - // First, generate biome data for all chunks - biome_pass(tilemap); - - // Then, generate base tile types based on biomes - base_tile_type_pass(tilemap); - - // Finally, fill small holes in the terrain - hole_fill_pass(tilemap); -} - void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) { BaseTileTypeGenerationPass pass(config_, master_rng_); master_rng_ = master_rng_.jump_96(); pass(tilemap); } +void TerrainGenerator::smoothen_mountains_pass(TileMap &tilemap) { + SmoothenMountainsPass pass(config_, master_rng_); + master_rng_ = master_rng_.jump_96(); + pass(tilemap); +} + void TerrainGenerator::hole_fill_pass(TileMap &tilemap) { HoleFillPass pass(config_); pass(tilemap); diff --git a/tilemap/src/noise.cpp b/tilemap/src/noise.cpp index b9c8516..c7c6b63 100644 --- a/tilemap/src/noise.cpp +++ b/tilemap/src/noise.cpp @@ -7,6 +7,42 @@ namespace istd { +DiscreteRandomNoise::DiscreteRandomNoise(Xoroshiro128PP rng) { + mask = rng.next(); + std::iota(permutation_.begin(), permutation_.end(), 0); + std::shuffle(permutation_.begin(), permutation_.end(), rng); +} + +std::uint8_t DiscreteRandomNoise::perm(int x) const { + // Map x to [0, 255] range + x &= 0xFF; + return permutation_[x]; +} + +std::uint32_t DiscreteRandomNoise::map(std::uint32_t x) const { + std::uint8_t a = x & 0xFF; + std::uint8_t b = (x >> 8) & 0xFF; + std::uint8_t c = (x >> 16) & 0xFF; + std::uint8_t d = (x >> 24) & 0xFF; + a = perm(a); + b = perm(b ^ a); + c = perm(c ^ b); + d = perm(d ^ c); + return (d << 24U) | (c << 16U) | (b << 8U) | a; +} + +std::uint64_t DiscreteRandomNoise::noise( + std::uint32_t x, std::uint32_t y, std::uint32_t z +) const { + auto A = map(x); + auto B = map(y ^ A); + auto C = map(z ^ B); + auto D = map(z); + auto E = map(y ^ D); + auto F = map(x ^ E); + return ((static_cast(C) << 32) | F) ^ mask; +} + PerlinNoise::PerlinNoise(Xoroshiro128PP rng) { // Initialize permutation array with values 0-255 permutation_.resize(256); diff --git a/tilemap/src/tilemap.cpp b/tilemap/src/tilemap.cpp index 0e29d92..36307d0 100644 --- a/tilemap/src/tilemap.cpp +++ b/tilemap/src/tilemap.cpp @@ -61,4 +61,40 @@ void TileMap::set_tile(TilePos pos, const Tile &tile) { chunks_[pos.chunk_x][pos.chunk_y].tiles[pos.local_x][pos.local_y] = tile; } +bool TileMap::is_at_boundary(TilePos pos) const { + std::uint8_t map_size = get_size(); + std::uint32_t global_x = pos.chunk_x * Chunk::size + pos.local_x; + std::uint32_t global_y = pos.chunk_y * Chunk::size + pos.local_y; + std::uint32_t max_global = map_size * Chunk::size - 1; + + return global_x == 0 || global_x == max_global || global_y == 0 + || global_y == max_global; +} + +std::vector TileMap::get_neighbors(TilePos pos, bool chebyshiv) const { + std::vector neighbors; + std::uint8_t map_size = get_size(); + + auto [global_x, global_y] = pos.to_global(); + int max_global = map_size * Chunk::size - 1; + + // Four cardinal directions + const int dx[] = {-1, 1, 0, 0, -1, 1, -1, 1}; + const int dy[] = {0, 0, -1, 1, -1, -1, 1, 1}; + for (int i = 0; i < (chebyshiv ? 8 : 4); ++i) { + int new_global_x = global_x + dx[i]; + int new_global_y = global_y + dy[i]; + + // Check bounds + if (new_global_x >= 0 && new_global_x <= max_global && new_global_y >= 0 + && new_global_y <= max_global) { + neighbors.push_back( + TilePos::from_global(new_global_x, new_global_y) + ); + } + } + + return neighbors; +} + } // namespace istd