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:
parent
6a79e7b0a5
commit
c5c62000a2
@ -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<TilePos> 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<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
|
||||
@ -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<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
|
||||
|
||||
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
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
#ifndef ISTD_TILEMAP_CHUNK_H
|
||||
#define ISTD_TILEMAP_CHUNK_H
|
||||
#include "tile.h"
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
|
||||
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<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 {
|
||||
// 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<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos);
|
||||
|
||||
} // namespace istd
|
||||
|
@ -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<std::vector<bool>> &visited, std::vector<TilePos> &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<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
|
||||
* @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<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
|
||||
@ -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
|
||||
|
@ -2,11 +2,47 @@
|
||||
#define ISTD_TILEMAP_NOISE_H
|
||||
|
||||
#include "xoroshiro.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
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 {
|
||||
private:
|
||||
std::vector<int> permutation_;
|
||||
|
@ -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<TilePos> get_neighbors(
|
||||
TilePos pos, bool chebyshev = false
|
||||
) const;
|
||||
};
|
||||
|
||||
} // namespace istd
|
||||
|
@ -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<std::uint8_t, std::uint8_t> 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<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
|
||||
|
@ -1,7 +1,9 @@
|
||||
#include "generation.h"
|
||||
#include "biome.h"
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
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<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;
|
||||
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<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;
|
||||
|
||||
std::vector<TilePos> 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<TilePos> HoleFillPass::get_neighbors(
|
||||
TileMap &tilemap, TilePos pos
|
||||
) const {
|
||||
std::vector<TilePos> 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();
|
||||
|
||||
// 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}
|
||||
std::vector<std::vector<bool>> visited(
|
||||
map_size * Chunk::size, std::vector<bool>(map_size * Chunk::size, false)
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
// Skip if already visited
|
||||
if (visited[global_x][global_y]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Tile &tile = tilemap.get_tile(pos);
|
||||
if (tile.base != BaseTileType::Mountain) {
|
||||
visited[global_x][global_y] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find connected component of mountains
|
||||
std::vector<TilePos> component_positions;
|
||||
std::uint32_t component_size = bfs_component_size(
|
||||
tilemap, pos, visited, component_positions
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
// Skip if it touches the boundary
|
||||
if (touches_boundary) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
// If the component is too small, smooth it out
|
||||
if (component_size <= config_.mountain_remove_threshold) {
|
||||
demountainize(tilemap, component_positions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return global_x == 0 || global_x == max_global || global_y == 0
|
||||
|| global_y == max_global;
|
||||
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]++;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t SmoothenMountainsPass::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);
|
||||
|
||||
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<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)
|
||||
: 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);
|
||||
|
@ -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<std::uint64_t>(C) << 32) | F) ^ mask;
|
||||
}
|
||||
|
||||
PerlinNoise::PerlinNoise(Xoroshiro128PP rng) {
|
||||
// Initialize permutation array with values 0-255
|
||||
permutation_.resize(256);
|
||||
|
@ -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<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
|
||||
|
Loading…
x
Reference in New Issue
Block a user