feat: add hole filling pass to terrain generation for improved terrain continuity
Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
parent
937091a40e
commit
6a79e7b0a5
@ -8,7 +8,7 @@ The tilemap library provides a flexible system for generating and managing tile-
|
||||
- **Chunk**: 64x64 tile containers with biome information
|
||||
- **Tile**: Individual map tiles with base and surface types
|
||||
- **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
|
||||
|
||||
## Core Classes
|
||||
@ -120,19 +120,22 @@ struct GenerationConfig {
|
||||
Seed seed; // 128-bit seed for random generation
|
||||
|
||||
// Temperature noise parameters
|
||||
double temperature_scale = /*default value*/; // Scale for temperature noise
|
||||
int temperature_octaves = /*default value*/; // Number of octaves for temperature noise
|
||||
double temperature_persistence = /*default value*/; // Persistence for temperature noise
|
||||
double temperature_scale = 0.05; // Scale for temperature noise
|
||||
int temperature_octaves = 3; // Number of octaves for temperature noise
|
||||
double temperature_persistence = 0.4; // Persistence for temperature noise
|
||||
|
||||
// Humidity noise parameters
|
||||
double humidity_scale = /*default value*/; // Scale for humidity noise
|
||||
int humidity_octaves = /*default value*/; // Number of octaves for humidity noise
|
||||
double humidity_persistence = /*default value*/; // Persistence for humidity noise
|
||||
double humidity_scale = 0.05; // Scale for humidity noise
|
||||
int humidity_octaves = 3; // Number of octaves for humidity noise
|
||||
double humidity_persistence = 0.4; // Persistence for humidity noise
|
||||
|
||||
// Base terrain noise parameters
|
||||
double base_scale = /*default value*/; // Scale for base terrain noise
|
||||
int base_octaves = /*default value*/; // Number of octaves for base terrain noise
|
||||
double base_persistence = /*default value*/; // Persistence 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
|
||||
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_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)
|
||||
|
||||
### Generation Passes
|
||||
|
||||
@ -208,6 +212,35 @@ private:
|
||||
- **Noise-based Distribution**: Uses calibrated noise for balanced terrain distribution
|
||||
- **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
|
||||
|
||||
Main orchestrator class that manages the generation process using multiple passes.
|
||||
@ -221,12 +254,14 @@ public:
|
||||
private:
|
||||
void biome_pass(TileMap& tilemap);
|
||||
void base_tile_type_pass(TileMap& tilemap);
|
||||
void hole_fill_pass(TileMap& tilemap);
|
||||
};
|
||||
```
|
||||
|
||||
**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
|
||||
3. **Hole Fill Pass**: Fill small holes in the terrain using BFS algorithm
|
||||
|
||||
### Generation Function
|
||||
|
||||
@ -393,6 +428,9 @@ config.base_scale = 0.08;
|
||||
config.base_octaves = 3;
|
||||
config.base_persistence = 0.5;
|
||||
|
||||
// Hole filling settings
|
||||
config.fill_threshold = 16; // Fill holes smaller than 16 tiles
|
||||
|
||||
// Generate terrain
|
||||
istd::map_generate(tilemap, config);
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include "tilemap.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
namespace istd {
|
||||
@ -26,6 +27,9 @@ struct GenerationConfig {
|
||||
double base_scale = 0.08; // Scale 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
|
||||
|
||||
// Hole filling parameters
|
||||
std::uint32_t fill_threshold = 16; // Fill holes smaller than this size
|
||||
};
|
||||
|
||||
class BiomeGenerationPass {
|
||||
@ -120,6 +124,61 @@ public:
|
||||
) 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
|
||||
class TerrainGenerator {
|
||||
private:
|
||||
@ -151,6 +210,12 @@ private:
|
||||
* @param tilemap The tilemap to generate base types into
|
||||
*/
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -164,6 +164,168 @@ BaseTileType BaseTileTypeGenerationPass::determine_base_type(
|
||||
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)
|
||||
: config_(config), master_rng_(config.seed) {}
|
||||
|
||||
@ -184,6 +346,9 @@ void TerrainGenerator::operator()(TileMap &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) {
|
||||
@ -192,6 +357,11 @@ void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) {
|
||||
pass(tilemap);
|
||||
}
|
||||
|
||||
void TerrainGenerator::hole_fill_pass(TileMap &tilemap) {
|
||||
HoleFillPass pass(config_);
|
||||
pass(tilemap);
|
||||
}
|
||||
|
||||
void map_generate(TileMap &tilemap, const GenerationConfig &config) {
|
||||
TerrainGenerator generator(config);
|
||||
generator(tilemap);
|
||||
|
Loading…
x
Reference in New Issue
Block a user