diff --git a/tilemap/CMakeLists.txt b/tilemap/CMakeLists.txt index d7242d1..6388a00 100644 --- a/tilemap/CMakeLists.txt +++ b/tilemap/CMakeLists.txt @@ -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 diff --git a/tilemap/examples/biome_demo.cpp b/tilemap/examples/biome_demo.cpp index d42c79c..486d77b 100644 --- a/tilemap/examples/biome_demo.cpp +++ b/tilemap/examples/biome_demo.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) { diff --git a/tilemap/include/generation.h b/tilemap/include/generation.h index 30185ee..13a2aeb 100644 --- a/tilemap/include/generation.h +++ b/tilemap/include/generation.h @@ -5,9 +5,7 @@ #include "chunk.h" #include "noise.h" #include "tilemap.h" -#include #include -#include #include 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> &visited, std::vector &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> &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 diff --git a/tilemap/include/tile.h b/tilemap/include/tile.h index 225a770..b6f0f65 100644 --- a/tilemap/include/tile.h +++ b/tilemap/include/tile.h @@ -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); diff --git a/tilemap/src/biome.cpp b/tilemap/src/biome.cpp index 638b70c..50908d2 100644 --- a/tilemap/src/biome.cpp +++ b/tilemap/src/biome.cpp @@ -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, }, }; diff --git a/tilemap/src/generation.cpp b/tilemap/src/generation.cpp index 59994be..88af59f 100644 --- a/tilemap/src/generation.cpp +++ b/tilemap/src/generation.cpp @@ -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); } diff --git a/tilemap/src/pass/base_tile_type.cpp b/tilemap/src/pass/base_tile_type.cpp index 79480e7..95acbe5 100644 --- a/tilemap/src/pass/base_tile_type.cpp +++ b/tilemap/src/pass/base_tile_type.cpp @@ -1,6 +1,7 @@ #include "biome.h" #include "chunk.h" #include "generation.h" +#include 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; diff --git a/tilemap/src/pass/smoothen_island.cpp b/tilemap/src/pass/smoothen_island.cpp new file mode 100644 index 0000000..09915b1 --- /dev/null +++ b/tilemap/src/pass/smoothen_island.cpp @@ -0,0 +1,274 @@ +#include "biome.h" +#include "generation.h" +#include "tile.h" +#include +#include + +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> visited( + map_size * Chunk::size, std::vector(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 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> &visited, std::vector &positions +) { + std::queue 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 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> &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> 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