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:
parent
7f147a1293
commit
e7b9fd856f
@ -7,6 +7,7 @@ set(ISTD_TILEMAP_SRC
|
||||
src/pass/deepwater.cpp
|
||||
src/pass/mountain_hole_fill.cpp
|
||||
src/pass/smoothen_mountain.cpp
|
||||
src/pass/smoothen_island.cpp
|
||||
src/generation.cpp
|
||||
src/tilemap.cpp
|
||||
src/noise.cpp
|
||||
|
@ -134,7 +134,7 @@ int main(int argc, char *argv[]) {
|
||||
istd::Seed seed
|
||||
= istd::Seed::from_string(argc >= 2 ? argv[1] : "hello_world");
|
||||
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
|
||||
if (argc == 4) {
|
||||
|
@ -5,9 +5,7 @@
|
||||
#include "chunk.h"
|
||||
#include "noise.h"
|
||||
#include "tilemap.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
namespace istd {
|
||||
@ -28,13 +26,18 @@ struct GenerationConfig {
|
||||
double humidity_persistence = 0.4; // Persistence for humidity noise
|
||||
|
||||
double base_scale = 0.08; // Scale for base terrain noise
|
||||
int base_octaves = 3; // Number of octaves for base terrain noise
|
||||
double base_persistence = 0.5; // Persistence for base terrain noise
|
||||
int base_octaves = 3; // Number of octaves 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 deepwater_radius = 2; // Radius for deepwater generation
|
||||
};
|
||||
@ -222,6 +225,72 @@ public:
|
||||
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
|
||||
class TerrainGenerator {
|
||||
private:
|
||||
@ -260,6 +329,12 @@ private:
|
||||
*/
|
||||
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
|
||||
* @param tilemap The tilemap to process
|
||||
|
@ -33,6 +33,7 @@ static_assert(surface_tile_count <= 16, "Surface tile don't fit in 4 bits");
|
||||
struct Tile {
|
||||
BaseTileType base : 4;
|
||||
SurfaceTileType surface : 4;
|
||||
friend bool operator==(Tile lhs, Tile rhs) = default;
|
||||
};
|
||||
|
||||
static_assert(sizeof(Tile) == 1);
|
||||
|
@ -36,10 +36,10 @@ constexpr BiomeProperties biome_properties[] = {
|
||||
.temperature = BiomeTemperature::Cold,
|
||||
.humidity = BiomeHumidity::Wet,
|
||||
.is_ocean = true,
|
||||
.water_ratio = .1,
|
||||
.ice_ratio = .7,
|
||||
.sand_ratio = .25,
|
||||
.land_ratio = .05,
|
||||
.water_ratio = .15,
|
||||
.ice_ratio = .8,
|
||||
.sand_ratio = .05,
|
||||
.land_ratio = .0,
|
||||
},
|
||||
// Plains (Temperate & Dry)
|
||||
{
|
||||
@ -69,10 +69,10 @@ constexpr BiomeProperties biome_properties[] = {
|
||||
.temperature = BiomeTemperature::Temperate,
|
||||
.humidity = BiomeHumidity::Wet,
|
||||
.is_ocean = true,
|
||||
.water_ratio = .8,
|
||||
.water_ratio = .95,
|
||||
.ice_ratio = .0,
|
||||
.sand_ratio = .15,
|
||||
.land_ratio = .05,
|
||||
.sand_ratio = .03,
|
||||
.land_ratio = .02,
|
||||
},
|
||||
// Desert (Hot & Dry)
|
||||
{
|
||||
@ -82,7 +82,7 @@ constexpr BiomeProperties biome_properties[] = {
|
||||
.is_ocean = false,
|
||||
.water_ratio = .0,
|
||||
.ice_ratio = .0,
|
||||
.sand_ratio = .8,
|
||||
.sand_ratio = .85,
|
||||
.land_ratio = .0,
|
||||
},
|
||||
// Savanna (Hot & Moderate)
|
||||
@ -102,10 +102,10 @@ constexpr BiomeProperties biome_properties[] = {
|
||||
.temperature = BiomeTemperature::Hot,
|
||||
.humidity = BiomeHumidity::Wet,
|
||||
.is_ocean = true,
|
||||
.water_ratio = .8,
|
||||
.water_ratio = .95,
|
||||
.ice_ratio = .0,
|
||||
.sand_ratio = .05,
|
||||
.land_ratio = .15,
|
||||
.sand_ratio = .01,
|
||||
.land_ratio = .04,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
#include "generation.h"
|
||||
#include "biome.h"
|
||||
|
||||
namespace istd {
|
||||
TerrainGenerator::TerrainGenerator(const GenerationConfig &config)
|
||||
@ -9,6 +8,7 @@ void TerrainGenerator::operator()(TileMap &tilemap) {
|
||||
biome_pass(tilemap);
|
||||
base_tile_type_pass(tilemap);
|
||||
smoothen_mountains_pass(tilemap);
|
||||
smoothen_islands_pass(tilemap);
|
||||
mountain_hole_fill_pass(tilemap);
|
||||
deepwater_pass(tilemap);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#include "biome.h"
|
||||
#include "chunk.h"
|
||||
#include "generation.h"
|
||||
#include <utility>
|
||||
|
||||
namespace istd {
|
||||
|
||||
@ -57,12 +58,14 @@ void BaseTileTypeGenerationPass::generate_subchunk(
|
||||
double global_y = chunk_y * Chunk::size + local_y;
|
||||
|
||||
// Generate base terrain noise value using uniform distribution
|
||||
double base_noise_value
|
||||
= base_noise_.uniform_noise(global_x, global_y);
|
||||
double base_noise_value = base_noise_.uniform_noise(
|
||||
global_x, global_y
|
||||
);
|
||||
|
||||
// Determine base terrain type
|
||||
BaseTileType base_type
|
||||
= determine_base_type(base_noise_value, properties);
|
||||
BaseTileType base_type = determine_base_type(
|
||||
base_noise_value, properties
|
||||
);
|
||||
|
||||
// Create tile with base and surface components
|
||||
Tile tile;
|
||||
|
274
tilemap/src/pass/smoothen_island.cpp
Normal file
274
tilemap/src/pass/smoothen_island.cpp
Normal 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
|
Loading…
x
Reference in New Issue
Block a user