feat: add island smoothing pass and update biome properties for improved terrain generation

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2025-08-03 01:27:56 +08:00
parent 7f147a1293
commit e7b9fd856f
Signed by: szTom
GPG Key ID: 072D999D60C6473C
8 changed files with 379 additions and 25 deletions

View File

@ -7,6 +7,7 @@ set(ISTD_TILEMAP_SRC
src/pass/deepwater.cpp src/pass/deepwater.cpp
src/pass/mountain_hole_fill.cpp src/pass/mountain_hole_fill.cpp
src/pass/smoothen_mountain.cpp src/pass/smoothen_mountain.cpp
src/pass/smoothen_island.cpp
src/generation.cpp src/generation.cpp
src/tilemap.cpp src/tilemap.cpp
src/noise.cpp src/noise.cpp

View File

@ -134,7 +134,7 @@ int main(int argc, char *argv[]) {
istd::Seed seed istd::Seed seed
= istd::Seed::from_string(argc >= 2 ? argv[1] : "hello_world"); = istd::Seed::from_string(argc >= 2 ? argv[1] : "hello_world");
std::string output_filename = argc >= 3 ? argv[2] : "output.bmp"; std::string output_filename = argc >= 3 ? argv[2] : "output.bmp";
int chunks_per_side = 4; // Default value int chunks_per_side = 8; // Default value
// Parse optional chunks_per_side parameter // Parse optional chunks_per_side parameter
if (argc == 4) { if (argc == 4) {

View File

@ -5,9 +5,7 @@
#include "chunk.h" #include "chunk.h"
#include "noise.h" #include "noise.h"
#include "tilemap.h" #include "tilemap.h"
#include <array>
#include <cstdint> #include <cstdint>
#include <queue>
#include <vector> #include <vector>
namespace istd { namespace istd {
@ -28,13 +26,18 @@ struct GenerationConfig {
double humidity_persistence = 0.4; // Persistence for humidity noise double humidity_persistence = 0.4; // Persistence for humidity noise
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
int mountain_smoothen_steps = 2; // Number of steps for mountain smoothing
// cellular automata
std::uint32_t mountain_remove_threshold = 10; // Threshold for mountain
// removal
int island_smoothen_steps = 8; // Number of steps for island smoothing
// cellular automata
std::uint32_t island_remove_threshold = 8; // Threshold for island removal
int mountain_smoothen_steps
= 2; // Number of steps for mountain smoothing cellular automata
std::uint32_t mountain_remove_threshold
= 10; // Threshold for mountain removal
std::uint32_t fill_threshold = 10; // Fill holes smaller than this size std::uint32_t fill_threshold = 10; // Fill holes smaller than this size
std::uint32_t deepwater_radius = 2; // Radius for deepwater generation std::uint32_t deepwater_radius = 2; // Radius for deepwater generation
}; };
@ -222,6 +225,72 @@ public:
void operator()(TileMap &tilemap); void operator()(TileMap &tilemap);
}; };
class SmoothenIslandPass {
private:
const GenerationConfig &config_;
DiscreteRandomNoise noise_;
/**
* @brief Perform BFS to find connected component size for islands
* @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 Remove small island components to create smoother terrain
* @param tilemap The tilemap to process
*/
void remove_small_island(TileMap &tilemap);
/**
* @brief Smoothen islands with cellular automata
* @param tilemap The tilemap to process
*/
void smoothen_islands(TileMap &tilemap, std::uint32_t step_i);
void smoothen_islands_subchunk(
TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y,
SubChunkPos sub_pos, std::uint32_t step_i,
std::vector<std::pair<TilePos, Tile>> &replacements
);
struct CACtx {
BiomeType biome;
std::uint8_t rand;
int adj_land, adj_sand, adj_water; // Adjacent tile counts
};
Tile ca_tile(TilePos pos, Tile tile, const CACtx &ctx) const;
/**
* @brief Check if a tile is part of an island (Land or Sand)
* @param tile The tile to check
* @return True if the tile is Land or Sand
*/
bool is_island_tile(const Tile &tile) const;
public:
/**
* @brief Construct an island smoothing pass
* @param config Generation configuration parameters
* @param rng Random number generator for terrain replacement
*/
SmoothenIslandPass(const GenerationConfig &config, Xoroshiro128PP rng);
/**
* @brief Smoothen islands in the 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
class TerrainGenerator { class TerrainGenerator {
private: private:
@ -260,6 +329,12 @@ private:
*/ */
void smoothen_mountains_pass(TileMap &tilemap); void smoothen_mountains_pass(TileMap &tilemap);
/**
* @brief Smoothen islands in the terrain
* @param tilemap The tilemap to process
*/
void smoothen_islands_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

@ -33,6 +33,7 @@ static_assert(surface_tile_count <= 16, "Surface tile don't fit in 4 bits");
struct Tile { struct Tile {
BaseTileType base : 4; BaseTileType base : 4;
SurfaceTileType surface : 4; SurfaceTileType surface : 4;
friend bool operator==(Tile lhs, Tile rhs) = default;
}; };
static_assert(sizeof(Tile) == 1); static_assert(sizeof(Tile) == 1);

View File

@ -36,10 +36,10 @@ constexpr BiomeProperties biome_properties[] = {
.temperature = BiomeTemperature::Cold, .temperature = BiomeTemperature::Cold,
.humidity = BiomeHumidity::Wet, .humidity = BiomeHumidity::Wet,
.is_ocean = true, .is_ocean = true,
.water_ratio = .1, .water_ratio = .15,
.ice_ratio = .7, .ice_ratio = .8,
.sand_ratio = .25, .sand_ratio = .05,
.land_ratio = .05, .land_ratio = .0,
}, },
// Plains (Temperate & Dry) // Plains (Temperate & Dry)
{ {
@ -69,10 +69,10 @@ constexpr BiomeProperties biome_properties[] = {
.temperature = BiomeTemperature::Temperate, .temperature = BiomeTemperature::Temperate,
.humidity = BiomeHumidity::Wet, .humidity = BiomeHumidity::Wet,
.is_ocean = true, .is_ocean = true,
.water_ratio = .8, .water_ratio = .95,
.ice_ratio = .0, .ice_ratio = .0,
.sand_ratio = .15, .sand_ratio = .03,
.land_ratio = .05, .land_ratio = .02,
}, },
// Desert (Hot & Dry) // Desert (Hot & Dry)
{ {
@ -82,7 +82,7 @@ constexpr BiomeProperties biome_properties[] = {
.is_ocean = false, .is_ocean = false,
.water_ratio = .0, .water_ratio = .0,
.ice_ratio = .0, .ice_ratio = .0,
.sand_ratio = .8, .sand_ratio = .85,
.land_ratio = .0, .land_ratio = .0,
}, },
// Savanna (Hot & Moderate) // Savanna (Hot & Moderate)
@ -102,10 +102,10 @@ constexpr BiomeProperties biome_properties[] = {
.temperature = BiomeTemperature::Hot, .temperature = BiomeTemperature::Hot,
.humidity = BiomeHumidity::Wet, .humidity = BiomeHumidity::Wet,
.is_ocean = true, .is_ocean = true,
.water_ratio = .8, .water_ratio = .95,
.ice_ratio = .0, .ice_ratio = .0,
.sand_ratio = .05, .sand_ratio = .01,
.land_ratio = .15, .land_ratio = .04,
}, },
}; };

View File

@ -1,5 +1,4 @@
#include "generation.h" #include "generation.h"
#include "biome.h"
namespace istd { namespace istd {
TerrainGenerator::TerrainGenerator(const GenerationConfig &config) TerrainGenerator::TerrainGenerator(const GenerationConfig &config)
@ -9,6 +8,7 @@ void TerrainGenerator::operator()(TileMap &tilemap) {
biome_pass(tilemap); biome_pass(tilemap);
base_tile_type_pass(tilemap); base_tile_type_pass(tilemap);
smoothen_mountains_pass(tilemap); smoothen_mountains_pass(tilemap);
smoothen_islands_pass(tilemap);
mountain_hole_fill_pass(tilemap); mountain_hole_fill_pass(tilemap);
deepwater_pass(tilemap); deepwater_pass(tilemap);
} }

View File

@ -1,6 +1,7 @@
#include "biome.h" #include "biome.h"
#include "chunk.h" #include "chunk.h"
#include "generation.h" #include "generation.h"
#include <utility>
namespace istd { namespace istd {
@ -57,12 +58,14 @@ void BaseTileTypeGenerationPass::generate_subchunk(
double global_y = chunk_y * Chunk::size + local_y; double global_y = chunk_y * Chunk::size + local_y;
// Generate base terrain noise value using uniform distribution // Generate base terrain noise value using uniform distribution
double base_noise_value double base_noise_value = base_noise_.uniform_noise(
= base_noise_.uniform_noise(global_x, global_y); global_x, global_y
);
// Determine base terrain type // Determine base terrain type
BaseTileType base_type BaseTileType base_type = determine_base_type(
= determine_base_type(base_noise_value, properties); base_noise_value, properties
);
// Create tile with base and surface components // Create tile with base and surface components
Tile tile; Tile tile;

View File

@ -0,0 +1,274 @@
#include "biome.h"
#include "generation.h"
#include "tile.h"
#include <algorithm>
#include <queue>
namespace istd {
SmoothenIslandPass::SmoothenIslandPass(
const GenerationConfig &config, Xoroshiro128PP rng
)
: config_(config), noise_(rng) {}
void SmoothenIslandPass::operator()(TileMap &tilemap) {
remove_small_island(tilemap);
for (int i = 1; i <= config_.island_smoothen_steps; ++i) {
smoothen_islands(tilemap, i);
}
remove_small_island(tilemap);
}
bool SmoothenIslandPass::is_island_tile(const Tile &tile) const {
return !(
tile.base == BaseTileType::Water || tile.base == BaseTileType::Deepwater
|| tile.base == BaseTileType::Ice
);
}
void SmoothenIslandPass::remove_small_island(TileMap &tilemap) {
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)
);
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 (!is_island_tile(tile)) {
visited[global_x][global_y] = true;
continue;
}
// Find connected component of island tiles
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;
}
}
// Skip if it touches the boundary
if (touches_boundary) {
continue;
}
// If the component is too small, convert it to water
if (component_size <= config_.island_remove_threshold) {
for (const auto &island_pos : component_positions) {
Tile tile = tilemap.get_tile(island_pos);
tile.base = BaseTileType::Water;
tilemap.set_tile(island_pos, tile);
}
}
}
}
}
}
}
std::uint32_t SmoothenIslandPass::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 (is_island_tile(neighbor_tile)) {
visited[neighbor_global_x][neighbor_global_y] = true;
queue.push(neighbor);
}
}
}
return size;
}
Tile SmoothenIslandPass::ca_tile(
TilePos pos, Tile tile, const CACtx &ctx
) const {
constexpr std::uint8_t as_water_chance_map[9] = {
0, 0, 0, 8, 16, 32, 64, 128, 255
};
// Sand -> Water
auto as_water_chance = as_water_chance_map[ctx.adj_water];
if (as_water_chance > 0 && tile.base == BaseTileType::Sand) {
if (ctx.rand < as_water_chance) {
tile.base = BaseTileType::Water;
return tile;
}
}
// Water -> Sand
if (!is_island_tile(tile)) {
int as_sand_chance = std::clamp(
ctx.adj_sand * 8 + ctx.adj_land * 32, 0, 255
);
if (ctx.rand < as_sand_chance) {
tile.base = BaseTileType::Sand;
}
return tile;
}
// Sand -> Land
if (tile.base == BaseTileType::Sand && ctx.biome == BiomeType::LukeOcean) {
int as_land_chance = std::clamp(
256 - ctx.adj_water * 32 - ctx.adj_sand * 12, 0, 255
);
if (ctx.rand < as_land_chance) {
tile.base = BaseTileType::Land;
}
return tile;
}
// Land -> Sand
if (tile.base == BaseTileType::Land) {
int as_sand_chance = std::clamp(
ctx.adj_water * 32 + ctx.adj_sand * 8, 0, 255
);
if (ctx.rand < as_sand_chance) {
tile.base = BaseTileType::Sand;
}
}
return tile;
}
void SmoothenIslandPass::smoothen_islands_subchunk(
TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y,
SubChunkPos sub_pos, std::uint32_t step_i,
std::vector<std::pair<TilePos, Tile>> &replacements
) {
const auto &chunk = tilemap.get_chunk(chunk_x, chunk_y);
auto biome = chunk.get_biome(sub_pos);
auto biome_props = get_biome_properties(biome);
if (!biome_props.is_ocean) {
// Only process ocean biomes
return;
}
auto [start_x, start_y] = subchunk_to_tile_start(sub_pos);
for (std::uint8_t local_x = start_x;
local_x < start_x + Chunk::subchunk_size; ++local_x) {
for (std::uint8_t local_y = start_y;
local_y < start_y + Chunk::subchunk_size; ++local_y) {
TilePos pos{chunk_x, chunk_y, local_x, local_y};
Tile tile = tilemap.get_tile(pos);
auto neighbors = tilemap.get_neighbors(pos, true);
if (neighbors.size() < 8) {
continue;
}
int adj_land = 0, adj_sand = 0, adj_water = 0;
for (auto neighbor : neighbors) {
const Tile &neighbor_tile = tilemap.get_tile(neighbor);
switch (neighbor_tile.base) {
case BaseTileType::Land:
++adj_land;
break;
case BaseTileType::Sand:
++adj_sand;
break;
case BaseTileType::Water:
case BaseTileType::Deepwater:
case BaseTileType::Ice:
++adj_water;
break;
default:
break; // Ignore other tile types
}
}
auto [global_x, global_y] = pos.to_global();
std::uint8_t rand = noise_.noise(global_x, global_y, step_i);
CACtx ctx{
biome, rand, adj_land, adj_sand, adj_water,
};
Tile new_tile = ca_tile(pos, tile, ctx);
if (new_tile != tile) {
replacements.emplace_back(pos, new_tile);
}
}
}
for (auto [pos, new_tile] : replacements) {
tilemap.set_tile(pos, new_tile);
}
}
void SmoothenIslandPass::smoothen_islands(
TileMap &tilemap, std::uint32_t step_i
) {
std::vector<std::pair<TilePos, Tile>> replacements;
for (std::uint8_t chunk_x = 0; chunk_x < tilemap.get_size(); ++chunk_x) {
for (std::uint8_t chunk_y = 0; chunk_y < tilemap.get_size();
++chunk_y) {
for (std::uint8_t sub_x = 0; sub_x < Chunk::subchunk_count;
++sub_x) {
for (std::uint8_t sub_y = 0; sub_y < Chunk::subchunk_count;
++sub_y) {
SubChunkPos sub_pos{sub_x, sub_y};
smoothen_islands_subchunk(
tilemap, chunk_x, chunk_y, sub_pos, step_i, replacements
);
}
}
}
}
}
void TerrainGenerator::smoothen_islands_pass(TileMap &tilemap) {
SmoothenIslandPass pass(config_, master_rng_);
master_rng_ = master_rng_.jump_96();
pass(tilemap);
}
} // namespace istd