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
|
- **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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user