feat: add hole filling pass to terrain generation for improved terrain continuity

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2025-08-02 14:19:03 +08:00
parent 937091a40e
commit 6a79e7b0a5
Signed by: szTom
GPG Key ID: 072D999D60C6473C
3 changed files with 283 additions and 10 deletions

View File

@ -8,7 +8,7 @@ The tilemap library provides a flexible system for generating and managing tile-
- **Chunk**: 64x64 tile containers with biome information - **Chunk**: 64x64 tile containers with biome information
- **Tile**: Individual map tiles with base and surface types - **Tile**: Individual map tiles with base and surface types
- **TerrainGenerator**: Pass-based procedural terrain generation system - **TerrainGenerator**: Pass-based procedural terrain generation system
- **Generation Passes**: Modular generation components (biome, base terrain) - **Generation Passes**: Modular generation components (biome, base terrain, hole filling)
- **Biome System**: Climate-based terrain variation - **Biome System**: Climate-based terrain variation
## Core Classes ## Core Classes
@ -120,19 +120,22 @@ struct GenerationConfig {
Seed seed; // 128-bit seed for random generation Seed seed; // 128-bit seed for random generation
// Temperature noise parameters // Temperature noise parameters
double temperature_scale = /*default value*/; // Scale for temperature noise double temperature_scale = 0.05; // Scale for temperature noise
int temperature_octaves = /*default value*/; // Number of octaves for temperature noise int temperature_octaves = 3; // Number of octaves for temperature noise
double temperature_persistence = /*default value*/; // Persistence for temperature noise double temperature_persistence = 0.4; // Persistence for temperature noise
// Humidity noise parameters // Humidity noise parameters
double humidity_scale = /*default value*/; // Scale for humidity noise double humidity_scale = 0.05; // Scale for humidity noise
int humidity_octaves = /*default value*/; // Number of octaves for humidity noise int humidity_octaves = 3; // Number of octaves for humidity noise
double humidity_persistence = /*default value*/; // Persistence for humidity noise double humidity_persistence = 0.4; // Persistence for humidity noise
// Base terrain noise parameters // Base terrain noise parameters
double base_scale = /*default value*/; // Scale for base terrain noise double base_scale = 0.08; // Scale for base terrain noise
int base_octaves = /*default value*/; // Number of octaves for base terrain noise int base_octaves = 3; // Number of octaves for base terrain noise
double base_persistence = /*default value*/; // Persistence 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
}; };
``` ```
@ -148,6 +151,7 @@ struct GenerationConfig {
- `base_scale`: Controls the scale/frequency of base terrain height variation - `base_scale`: Controls the scale/frequency of base terrain height variation
- `base_octaves`: Number of noise octaves for base terrain - `base_octaves`: Number of noise octaves for base terrain
- `base_persistence`: How much each octave contributes to base terrain noise (0.0-1.0) - `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)
### Generation Passes ### Generation Passes
@ -208,6 +212,35 @@ private:
- **Noise-based Distribution**: Uses calibrated noise for balanced terrain distribution - **Noise-based Distribution**: Uses calibrated noise for balanced terrain distribution
- **Tile-level Detail**: Generates terrain at individual tile resolution - **Tile-level Detail**: Generates terrain at individual tile resolution
#### HoleFillPass
Fills small holes in the terrain using breadth-first search (BFS) algorithm.
```cpp
class HoleFillPass {
public:
explicit HoleFillPass(const GenerationConfig& config);
void operator()(TileMap& tilemap);
private:
bool is_passable(BaseTileType type) const;
std::uint32_t bfs_component_size(
TileMap& tilemap, TilePos start_pos,
std::vector<std::vector<bool>>& visited,
std::vector<TilePos>& positions
);
std::vector<TilePos> get_neighbors(TileMap& tilemap, TilePos pos) const;
bool is_at_boundary(TileMap& tilemap, TilePos pos) const;
};
```
**Key Features:**
- **BFS Algorithm**: Uses breadth-first search to identify connected components
- **Boundary Awareness**: Preserves holes that touch the map boundary
- **Size-based Filtering**: Only fills holes smaller than `fill_threshold`
- **Mountain-as-Impassable**: Treats mountains as impassable terrain for connectivity
- **Hole Filling**: Converts small isolated areas to mountains for cleaner terrain
### TerrainGenerator ### TerrainGenerator
Main orchestrator class that manages the generation process using multiple passes. Main orchestrator class that manages the generation process using multiple passes.
@ -221,12 +254,14 @@ public:
private: private:
void biome_pass(TileMap& tilemap); void biome_pass(TileMap& tilemap);
void base_tile_type_pass(TileMap& tilemap); void base_tile_type_pass(TileMap& tilemap);
void hole_fill_pass(TileMap& tilemap);
}; };
``` ```
**Generation Flow:** **Generation Flow:**
1. **Biome Pass**: Generate climate data and assign biomes to sub-chunks 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 2. **Base Tile Type Pass**: Generate base terrain types based on biomes and noise
3. **Hole Fill Pass**: Fill small holes in the terrain using BFS algorithm
### Generation Function ### Generation Function
@ -393,6 +428,9 @@ config.base_scale = 0.08;
config.base_octaves = 3; config.base_octaves = 3;
config.base_persistence = 0.5; config.base_persistence = 0.5;
// Hole filling settings
config.fill_threshold = 16; // Fill holes smaller than 16 tiles
// Generate terrain // Generate terrain
istd::map_generate(tilemap, config); istd::map_generate(tilemap, config);

View File

@ -7,6 +7,7 @@
#include "tilemap.h" #include "tilemap.h"
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <queue>
#include <vector> #include <vector>
namespace istd { namespace istd {
@ -26,6 +27,9 @@ struct GenerationConfig {
double base_scale = 0.08; // Scale for base terrain noise double base_scale = 0.08; // Scale for base terrain noise
int base_octaves = 3; // Number of octaves for base terrain noise int base_octaves = 3; // Number of octaves for base terrain noise
double base_persistence = 0.5; // Persistence 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
}; };
class BiomeGenerationPass { class BiomeGenerationPass {
@ -120,6 +124,61 @@ public:
) const; ) const;
}; };
class HoleFillPass {
private:
const GenerationConfig &config_;
public:
/**
* @brief Construct a hole fill pass
* @param config Generation configuration parameters
*/
explicit HoleFillPass(const GenerationConfig &config);
/**
* @brief Fill small holes in the terrain using BFS
* @param tilemap The tilemap to process
*/
void operator()(TileMap &tilemap);
private:
/**
* @brief Check if a tile type is passable for BFS
* @param type The base tile type to check
* @return True if the tile is passable (not mountain or at boundary)
*/
bool is_passable(BaseTileType type) const;
/**
* @brief Perform BFS to find connected component size
* @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::uint32_t bfs_component_size(
TileMap &tilemap, TilePos start_pos,
std::vector<std::vector<bool>> &visited, std::vector<TilePos> &positions
);
/**
* @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
*/
std::vector<TilePos> get_neighbors(TileMap &tilemap, TilePos pos) const;
/**
* @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
*/
bool is_at_boundary(TileMap &tilemap, TilePos pos) const;
};
// Terrain generator class that manages the generation process // Terrain generator class that manages the generation process
class TerrainGenerator { class TerrainGenerator {
private: private:
@ -151,6 +210,12 @@ private:
* @param tilemap The tilemap to generate base types into * @param tilemap The tilemap to generate base types into
*/ */
void base_tile_type_pass(TileMap &tilemap); void base_tile_type_pass(TileMap &tilemap);
/**
* @brief Fill small holes in the terrain
* @param tilemap The tilemap to process
*/
void hole_fill_pass(TileMap &tilemap);
}; };
/** /**

View File

@ -164,6 +164,168 @@ BaseTileType BaseTileTypeGenerationPass::determine_base_type(
std::unreachable(); std::unreachable();
} }
HoleFillPass::HoleFillPass(const GenerationConfig &config): config_(config) {}
void HoleFillPass::operator()(TileMap &tilemap) {
std::uint8_t map_size = tilemap.get_size();
std::uint32_t total_tiles = map_size * Chunk::size;
// Create visited array for the entire map
std::vector<std::vector<bool>> visited(
total_tiles, std::vector<bool>(total_tiles, false)
);
// Process all tiles in the map
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};
std::uint32_t global_x = chunk_x * Chunk::size + local_x;
std::uint32_t global_y = chunk_y * Chunk::size + local_y;
// Skip if already visited
if (visited[global_x][global_y]) {
continue;
}
const Tile &tile = tilemap.get_tile(pos);
// Only process passable tiles
if (!is_passable(tile.base)) {
visited[global_x][global_y] = true;
continue;
}
// Find connected component
std::vector<TilePos> component_positions;
std::uint32_t component_size = bfs_component_size(
tilemap, pos, visited, component_positions
);
// 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)) {
touches_boundary = true;
break;
}
}
// Fill small holes that don't touch the boundary
if (!touches_boundary
&& 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;
tilemap.set_tile(fill_pos, fill_tile);
}
}
}
}
}
}
}
bool HoleFillPass::is_passable(BaseTileType type) const {
return type != BaseTileType::Mountain;
}
std::uint32_t HoleFillPass::bfs_component_size(
TileMap &tilemap, TilePos start_pos,
std::vector<std::vector<bool>> &visited, std::vector<TilePos> &positions
) {
std::queue<TilePos> 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;
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<TilePos> 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;
if (visited[neighbor_global_x][neighbor_global_y]) {
continue;
}
const Tile &neighbor_tile = tilemap.get_tile(neighbor);
if (is_passable(neighbor_tile.base)) {
visited[neighbor_global_x][neighbor_global_y] = true;
queue.push(neighbor);
}
}
}
return size;
}
std::vector<TilePos> HoleFillPass::get_neighbors(
TileMap &tilemap, TilePos pos
) const {
std::vector<TilePos> neighbors;
std::uint8_t map_size = tilemap.get_size();
// 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;
// Four cardinal directions
const int dx[] = {-1, 1, 0, 0};
const int dy[] = {0, 0, -1, 1};
for (int i = 0; i < 4; ++i) {
int new_global_x = static_cast<int>(global_x) + dx[i];
int new_global_y = static_cast<int>(global_y) + dy[i];
// Check bounds
if (new_global_x >= 0 && new_global_x < static_cast<int>(max_global)
&& new_global_y >= 0
&& new_global_y < static_cast<int>(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;
neighbors.push_back(
{new_chunk_x, new_chunk_y, new_local_x, new_local_y}
);
}
}
return neighbors;
}
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;
return global_x == 0 || global_x == max_global || global_y == 0
|| global_y == max_global;
}
TerrainGenerator::TerrainGenerator(const GenerationConfig &config) TerrainGenerator::TerrainGenerator(const GenerationConfig &config)
: config_(config), master_rng_(config.seed) {} : config_(config), master_rng_(config.seed) {}
@ -184,6 +346,9 @@ void TerrainGenerator::operator()(TileMap &tilemap) {
// Then, generate base tile types based on biomes // Then, generate base tile types based on biomes
base_tile_type_pass(tilemap); base_tile_type_pass(tilemap);
// Finally, fill small holes in the terrain
hole_fill_pass(tilemap);
} }
void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) { void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) {
@ -192,6 +357,11 @@ void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) {
pass(tilemap); pass(tilemap);
} }
void TerrainGenerator::hole_fill_pass(TileMap &tilemap) {
HoleFillPass pass(config_);
pass(tilemap);
}
void map_generate(TileMap &tilemap, const GenerationConfig &config) { void map_generate(TileMap &tilemap, const GenerationConfig &config) {
TerrainGenerator generator(config); TerrainGenerator generator(config);
generator(tilemap); generator(tilemap);