feat: add SmoothenMountainsPass for terrain smoothing and enhance TileMap with boundary and neighbor methods

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2025-08-02 16:41:50 +08:00
parent 6a79e7b0a5
commit c5c62000a2
Signed by: szTom
GPG Key ID: 072D999D60C6473C
9 changed files with 489 additions and 85 deletions

View File

@ -29,12 +29,19 @@ public:
Tile& get_tile(TilePos pos); Tile& get_tile(TilePos pos);
const Tile& get_tile(TilePos pos) const; const Tile& get_tile(TilePos pos) const;
void set_tile(TilePos pos, const Tile& tile); void set_tile(TilePos pos, const Tile& tile);
bool is_at_boundary(TilePos pos) const;
std::vector<TilePos> get_neighbors(TilePos pos, bool chebyshev = false) const;
}; };
``` ```
**Constructor Parameters:** **Constructor Parameters:**
- `size`: Number of chunks per side (max 100), creating an n×n grid - `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 ### Chunk
Each chunk contains 64×64 tiles and sub-chunk biome information. 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 ```cpp
struct Chunk { struct Chunk {
static constexpr uint8_t size = 64; // Tiles per side 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 static constexpr uint8_t subchunk_count = size / subchunk_size; // Sub-chunks per side
Tile tiles[size][size]; // 64x64 tile grid Tile tiles[size][size]; // 64x64 tile grid
BiomeType biome[subchunk_count][subchunk_count]; // Sub-chunk biomes 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); BiomeType& get_biome(SubChunkPos pos);
const BiomeType& get_biome(SubChunkPos pos) const; const BiomeType& get_biome(SubChunkPos pos) const;
}; };
@ -79,7 +86,7 @@ struct Tile {
### TilePos ### TilePos
Position structure for locating tiles within the map. Position structure for locating tiles within the map with enhanced coordinate conversion support.
```cpp ```cpp
struct TilePos { struct TilePos {
@ -87,7 +94,14 @@ struct TilePos {
uint8_t chunk_y; // Chunk Y coordinate uint8_t chunk_y; // Chunk Y coordinate
uint8_t local_x; // Tile X within chunk (0-63) uint8_t local_x; // Tile X within chunk (0-63)
uint8_t local_y; // Tile Y within chunk (0-63) uint8_t local_y; // Tile Y within chunk (0-63)
// Coordinate conversion methods
std::pair<std::uint16_t, std::uint16_t> 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 ### SubChunkPos
@ -134,8 +148,11 @@ struct GenerationConfig {
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
// Mountain smoothing parameters
std::uint32_t mountain_remove_threshold = 10; // Remove mountain components smaller than this size
// Hole filling parameters // 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_scale`: Controls the scale/frequency of humidity variation across the map
- `humidity_octaves`: Number of noise octaves for humidity - `humidity_octaves`: Number of noise octaves for humidity
- `humidity_persistence`: How much each octave contributes to humidity noise (0.0-1.0) - `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_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) - `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 ### Generation Passes
@ -241,6 +259,33 @@ private:
- **Mountain-as-Impassable**: Treats mountains as impassable terrain for connectivity - **Mountain-as-Impassable**: Treats mountains as impassable terrain for connectivity
- **Hole Filling**: Converts small isolated areas to mountains for cleaner terrain - **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<std::vector<bool>>& visited,
std::vector<TilePos>& positions
);
void demountainize(TileMap& tilemap, const std::vector<TilePos>& 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 ### TerrainGenerator
Main orchestrator class that manages the generation process using multiple passes. Main orchestrator class that manages the generation process using multiple passes.
@ -254,10 +299,23 @@ 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 smoothen_mountains_pass(TileMap& tilemap);
void hole_fill_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:** **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
@ -321,6 +379,24 @@ public:
## Noise System ## 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 ### PerlinNoise
Standard Perlin noise implementation using Xoroshiro128++ for procedural generation. Standard Perlin noise implementation using Xoroshiro128++ for procedural generation.
@ -428,8 +504,11 @@ config.base_scale = 0.08;
config.base_octaves = 3; config.base_octaves = 3;
config.base_persistence = 0.5; config.base_persistence = 0.5;
// Mountain smoothing settings
config.mountain_remove_threshold = 10; // Remove mountain components smaller than 10 tiles
// Hole filling settings // 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 // Generate terrain
istd::map_generate(tilemap, config); 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 3. **Reproducible Results**: Same seed produces identical results across passes
4. **Extensibility**: Easy to add new passes or modify existing ones 4. **Extensibility**: Easy to add new passes or modify existing ones
5. **Performance**: Efficient memory access patterns and reduced redundant calculations 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 ## Thread Safety

View File

@ -1,6 +1,7 @@
#ifndef ISTD_TILEMAP_CHUNK_H #ifndef ISTD_TILEMAP_CHUNK_H
#define ISTD_TILEMAP_CHUNK_H #define ISTD_TILEMAP_CHUNK_H
#include "tile.h" #include "tile.h"
#include <compare>
#include <cstdint> #include <cstdint>
namespace istd { namespace istd {
@ -8,7 +9,9 @@ namespace istd {
// Forward declaration // Forward declaration
enum class BiomeType : std::uint8_t; 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 { struct SubChunkPos {
std::uint8_t sub_x; std::uint8_t sub_x;
std::uint8_t sub_y; std::uint8_t sub_y;
@ -23,8 +26,30 @@ struct TilePos {
uint8_t chunk_y; uint8_t chunk_y;
uint8_t local_x; uint8_t local_x;
uint8_t local_y; uint8_t local_y;
/**
* @brief Convert TilePos to global coordinates
* @return Pair of global X and Y coordinates
*/
std::pair<std::uint16_t, std::uint16_t> 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 { struct Chunk {
// Size of a chunk in tiles (64 x 64) // Size of a chunk in tiles (64 x 64)
static constexpr uint8_t size = 64; static constexpr uint8_t size = 64;
@ -41,18 +66,30 @@ struct Chunk {
// array of biomes for sub-chunks // array of biomes for sub-chunks
BiomeType biome[subchunk_count][subchunk_count]; 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) { BiomeType &get_biome(SubChunkPos pos) {
return biome[pos.sub_x][pos.sub_y]; 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 { const BiomeType &get_biome(SubChunkPos pos) const {
return biome[pos.sub_x][pos.sub_y]; 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<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos); std::pair<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos);
} // namespace istd } // namespace istd

View File

@ -12,6 +12,9 @@
namespace istd { namespace istd {
/**
* @brief Configuration parameters for terrain generation
*/
struct GenerationConfig { struct GenerationConfig {
Seed seed; Seed seed;
@ -28,8 +31,9 @@ struct GenerationConfig {
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 mountain_remove_threshold
std::uint32_t fill_threshold = 16; // Fill holes smaller than this size = 10; // Threshold for mountain removal
std::uint32_t fill_threshold = 10; // Fill holes smaller than this size
}; };
class BiomeGenerationPass { class BiomeGenerationPass {
@ -161,22 +165,46 @@ private:
TileMap &tilemap, TilePos start_pos, TileMap &tilemap, TilePos start_pos,
std::vector<std::vector<bool>> &visited, std::vector<TilePos> &positions std::vector<std::vector<bool>> &visited, std::vector<TilePos> &positions
); );
};
class SmoothenMountainsPass {
private:
const GenerationConfig &config_;
DiscreteRandomNoise noise_;
/** /**
* @brief Get all valid neighbors of a position * @brief Perform BFS to find connected component size for mountains
* @param tilemap The tilemap for bounds checking * @param tilemap The tilemap to search
* @param pos The position to get neighbors for * @param start_pos Starting position for BFS
* @return Vector of valid neighbor positions * @param visited 2D array tracking visited tiles
* @param positions Output vector of positions in this component
* @return Size of the connected component
*/ */
std::vector<TilePos> get_neighbors(TileMap &tilemap, TilePos pos) const; std::uint32_t bfs_component_size(
TileMap &tilemap, TilePos start_pos,
std::vector<std::vector<bool>> &visited, std::vector<TilePos> &positions
);
/** /**
* @brief Check if a position is at the map boundary * @brief Replace mountain tiles with terrain types from neighboring areas
* @param tilemap The tilemap for bounds checking * @param tilemap The tilemap to modify
* @param pos The position to check * @param positions Vector of mountain positions to replace
* @return True if the position is at the boundary
*/ */
bool is_at_boundary(TileMap &tilemap, TilePos pos) const; void demountainize(TileMap &tilemap, const std::vector<TilePos> &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 // Terrain generator class that manages the generation process
@ -211,6 +239,12 @@ private:
*/ */
void base_tile_type_pass(TileMap &tilemap); 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 * @brief Fill small holes in the terrain
* @param tilemap The tilemap to process * @param tilemap The tilemap to process

View File

@ -2,11 +2,47 @@
#define ISTD_TILEMAP_NOISE_H #define ISTD_TILEMAP_NOISE_H
#include "xoroshiro.h" #include "xoroshiro.h"
#include <array>
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
namespace istd { 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<std::uint8_t, 256> 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 { class PerlinNoise {
private: private:
std::vector<int> permutation_; std::vector<int> permutation_;

View File

@ -47,6 +47,24 @@ public:
* @param tile The tile to set * @param tile The tile to set
*/ */
void set_tile(TilePos pos, const Tile &tile); 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<TilePos> get_neighbors(
TilePos pos, bool chebyshev = false
) const;
}; };
} // namespace istd } // namespace istd

View File

@ -2,9 +2,38 @@
namespace istd { 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<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos) { std::pair<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos) {
// Convert sub-chunk position to tile start coordinates // Convert sub-chunk position to tile start coordinates
return {pos.sub_x * Chunk::subchunk_size, pos.sub_y * Chunk::subchunk_size}; return {pos.sub_x * Chunk::subchunk_size, pos.sub_y * Chunk::subchunk_size};
} }
std::pair<std::uint16_t, std::uint16_t> 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<uint8_t>(global_x / Chunk::size),
static_cast<uint8_t>(global_y / Chunk::size),
static_cast<uint8_t>(global_x % Chunk::size),
static_cast<uint8_t>(global_y % Chunk::size)
};
}
} // namespace istd } // namespace istd

View File

@ -1,7 +1,9 @@
#include "generation.h" #include "generation.h"
#include "biome.h" #include "biome.h"
#include <cmath> #include <cmath>
#include <map>
#include <random> #include <random>
#include <set>
#include <utility> #include <utility>
namespace istd { namespace istd {
@ -182,8 +184,7 @@ void HoleFillPass::operator()(TileMap &tilemap) {
for (std::uint8_t local_y = 0; local_y < Chunk::size; for (std::uint8_t local_y = 0; local_y < Chunk::size;
++local_y) { ++local_y) {
TilePos pos{chunk_x, chunk_y, local_x, local_y}; TilePos pos{chunk_x, chunk_y, local_x, local_y};
std::uint32_t global_x = chunk_x * Chunk::size + local_x; auto [global_x, global_y] = pos.to_global();
std::uint32_t global_y = chunk_y * Chunk::size + local_y;
// Skip if already visited // Skip if already visited
if (visited[global_x][global_y]) { if (visited[global_x][global_y]) {
@ -206,8 +207,8 @@ void HoleFillPass::operator()(TileMap &tilemap) {
// Check if this component touches the boundary // Check if this component touches the boundary
bool touches_boundary = false; bool touches_boundary = false;
for (const TilePos &component_pos : component_positions) { for (const auto component_pos : component_positions) {
if (is_at_boundary(tilemap, component_pos)) { if (tilemap.is_at_boundary(component_pos)) {
touches_boundary = true; touches_boundary = true;
break; break;
} }
@ -215,7 +216,7 @@ void HoleFillPass::operator()(TileMap &tilemap) {
// Fill small holes that don't touch the boundary // Fill small holes that don't touch the boundary
if (!touches_boundary if (!touches_boundary
&& component_size < config_.fill_threshold) { && component_size <= config_.fill_threshold) {
for (const TilePos &fill_pos : component_positions) { for (const TilePos &fill_pos : component_positions) {
Tile fill_tile = tilemap.get_tile(fill_pos); Tile fill_tile = tilemap.get_tile(fill_pos);
fill_tile.base = BaseTileType::Mountain; fill_tile.base = BaseTileType::Mountain;
@ -239,11 +240,7 @@ std::uint32_t HoleFillPass::bfs_component_size(
std::queue<TilePos> queue; std::queue<TilePos> queue;
queue.push(start_pos); queue.push(start_pos);
std::uint8_t map_size = tilemap.get_size(); auto [start_global_x, start_global_y] = start_pos.to_global();
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; visited[start_global_x][start_global_y] = true;
std::uint32_t size = 0; std::uint32_t size = 0;
@ -256,13 +253,9 @@ std::uint32_t HoleFillPass::bfs_component_size(
++size; ++size;
// Check all neighbors // Check all neighbors
std::vector<TilePos> neighbors = get_neighbors(tilemap, current); std::vector<TilePos> neighbors = tilemap.get_neighbors(current);
for (const TilePos &neighbor : neighbors) { for (const auto neighbor : neighbors) {
std::uint32_t neighbor_global_x auto [neighbor_global_x, neighbor_global_y] = neighbor.to_global();
= 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]) { if (visited[neighbor_global_x][neighbor_global_y]) {
continue; continue;
} }
@ -278,57 +271,159 @@ std::uint32_t HoleFillPass::bfs_component_size(
return size; return size;
} }
std::vector<TilePos> HoleFillPass::get_neighbors( SmoothenMountainsPass::SmoothenMountainsPass(
TileMap &tilemap, TilePos pos const GenerationConfig &config, Xoroshiro128PP rng
) const { )
std::vector<TilePos> neighbors; : config_(config), noise_(rng) {}
void SmoothenMountainsPass::operator()(TileMap &tilemap) {
std::uint8_t map_size = tilemap.get_size(); std::uint8_t map_size = tilemap.get_size();
std::vector<std::vector<bool>> visited(
map_size * Chunk::size, std::vector<bool>(map_size * Chunk::size, false)
);
// Calculate global coordinates for (std::uint8_t chunk_x = 0; chunk_x < map_size; ++chunk_x) {
std::uint32_t global_x = pos.chunk_x * Chunk::size + pos.local_x; for (std::uint8_t chunk_y = 0; chunk_y < map_size; ++chunk_y) {
std::uint32_t global_y = pos.chunk_y * Chunk::size + pos.local_y; for (std::uint8_t local_x = 0; local_x < Chunk::size; ++local_x) {
std::uint32_t max_global = map_size * Chunk::size; 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 // Skip if already visited
const int dx[] = {-1, 1, 0, 0}; if (visited[global_x][global_y]) {
const int dy[] = {0, 0, -1, 1}; continue;
}
for (int i = 0; i < 4; ++i) { const Tile &tile = tilemap.get_tile(pos);
int new_global_x = static_cast<int>(global_x) + dx[i]; if (tile.base != BaseTileType::Mountain) {
int new_global_y = static_cast<int>(global_y) + dy[i]; visited[global_x][global_y] = true;
continue;
}
// Check bounds // Find connected component of mountains
if (new_global_x >= 0 && new_global_x < static_cast<int>(max_global) std::vector<TilePos> component_positions;
&& new_global_y >= 0 std::uint32_t component_size = bfs_component_size(
&& new_global_y < static_cast<int>(max_global)) { tilemap, pos, visited, component_positions
// 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( // If the component touches the boundary, skip it
{new_chunk_x, new_chunk_y, new_local_x, new_local_y} 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<TilePos> &pos
) {
// Step 1: Look around the mountain to see what should replace it
std::map<BaseTileType, int> type_count;
std::set<TilePos> 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::uint32_t SmoothenMountainsPass::bfs_component_size(
std::uint8_t map_size = tilemap.get_size(); TileMap &tilemap, TilePos start_pos,
std::uint32_t global_x = pos.chunk_x * Chunk::size + pos.local_x; std::vector<std::vector<bool>> &visited, std::vector<TilePos> &positions
std::uint32_t global_y = pos.chunk_y * Chunk::size + pos.local_y; ) {
std::uint32_t max_global = map_size * Chunk::size - 1; std::queue<TilePos> queue;
queue.push(start_pos);
return global_x == 0 || global_x == max_global || global_y == 0 auto [start_global_x, start_global_y] = start_pos.to_global();
|| global_y == max_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<TilePos> 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) TerrainGenerator::TerrainGenerator(const GenerationConfig &config)
: config_(config), master_rng_(config.seed) {} : 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) { void TerrainGenerator::biome_pass(TileMap &tilemap) {
// Create two RNGs for temperature and humidity noise // Create two RNGs for temperature and humidity noise
Xoroshiro128PP temp_rng = master_rng_; Xoroshiro128PP temp_rng = master_rng_;
@ -340,23 +435,18 @@ void TerrainGenerator::biome_pass(TileMap &tilemap) {
biome_pass(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) { void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) {
BaseTileTypeGenerationPass pass(config_, master_rng_); BaseTileTypeGenerationPass pass(config_, master_rng_);
master_rng_ = master_rng_.jump_96(); master_rng_ = master_rng_.jump_96();
pass(tilemap); 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) { void TerrainGenerator::hole_fill_pass(TileMap &tilemap) {
HoleFillPass pass(config_); HoleFillPass pass(config_);
pass(tilemap); pass(tilemap);

View File

@ -7,6 +7,42 @@
namespace istd { 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<std::uint64_t>(C) << 32) | F) ^ mask;
}
PerlinNoise::PerlinNoise(Xoroshiro128PP rng) { PerlinNoise::PerlinNoise(Xoroshiro128PP rng) {
// Initialize permutation array with values 0-255 // Initialize permutation array with values 0-255
permutation_.resize(256); permutation_.resize(256);

View File

@ -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; 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<TilePos> TileMap::get_neighbors(TilePos pos, bool chebyshiv) const {
std::vector<TilePos> 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 } // namespace istd