diff --git a/tilemap/CMakeLists.txt b/tilemap/CMakeLists.txt index 1935df6..d7242d1 100644 --- a/tilemap/CMakeLists.txt +++ b/tilemap/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.27) # Define the tilemap library source files set(ISTD_TILEMAP_SRC + src/pass/base_tile_type.cpp + src/pass/biome.cpp + src/pass/deepwater.cpp + src/pass/mountain_hole_fill.cpp + src/pass/smoothen_mountain.cpp src/generation.cpp src/tilemap.cpp src/noise.cpp diff --git a/tilemap/include/generation.h b/tilemap/include/generation.h index bc3a4c1..30185ee 100644 --- a/tilemap/include/generation.h +++ b/tilemap/include/generation.h @@ -131,7 +131,7 @@ public: ) const; }; -class HoleFillPass { +class MountainHoleFillPass { private: const GenerationConfig &config_; @@ -140,7 +140,7 @@ public: * @brief Construct a hole fill pass * @param config Generation configuration parameters */ - explicit HoleFillPass(const GenerationConfig &config); + explicit MountainHoleFillPass(const GenerationConfig &config); /** * @brief Fill small holes in the terrain using BFS @@ -264,7 +264,7 @@ private: * @brief Fill small holes in the terrain * @param tilemap The tilemap to process */ - void hole_fill_pass(TileMap &tilemap); + void mountain_hole_fill_pass(TileMap &tilemap); /** * @brief Generate deepwater tiles in ocean biomes diff --git a/tilemap/src/generation.cpp b/tilemap/src/generation.cpp index a5817c7..59994be 100644 --- a/tilemap/src/generation.cpp +++ b/tilemap/src/generation.cpp @@ -1,489 +1,7 @@ #include "generation.h" #include "biome.h" -#include -#include -#include -#include -#include namespace istd { - -BiomeGenerationPass::BiomeGenerationPass( - const GenerationConfig &config, Xoroshiro128PP r1, Xoroshiro128PP r2 -) - : config_(config), temperature_noise_(r1), humidity_noise_(r2) { - temperature_noise_.calibrate( - config.temperature_scale, config.temperature_octaves, - config.temperature_persistence - ); - humidity_noise_.calibrate( - config.humidity_scale, config.humidity_octaves, - config.humidity_persistence - ); -} - -void BiomeGenerationPass::operator()(TileMap &tilemap) { - std::uint8_t map_size = tilemap.get_size(); - - // Generate biomes for each sub-chunk - 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) { - Chunk &chunk = tilemap.get_chunk(chunk_x, 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) { - // Calculate global position for this sub-chunk's center - auto [start_x, start_y] - = subchunk_to_tile_start(SubChunkPos(sub_x, sub_y)); - double global_x = chunk_x * Chunk::size + start_x - + Chunk::subchunk_size / 2; - - double global_y = chunk_y * Chunk::size + start_y - + Chunk::subchunk_size / 2; - - // Get climate values - auto [temperature, humidity] - = get_climate(global_x, global_y); - - // Determine biome and store directly in chunk - BiomeType biome = determine_biome(temperature, humidity); - chunk.biome[sub_x][sub_y] = biome; - } - } - } - } -} - -std::pair BiomeGenerationPass::get_climate( - double global_x, double global_y -) const { - // Generate temperature noise (0-1 range) - double temperature = temperature_noise_.uniform_noise( - global_x * config_.temperature_scale, - global_y * config_.temperature_scale - ); - - // Generate humidity noise (0-1 range) - double humidity = humidity_noise_.uniform_noise( - global_x * config_.humidity_scale, global_y * config_.humidity_scale - ); - - return {temperature, humidity}; -} - -BaseTileTypeGenerationPass::BaseTileTypeGenerationPass( - const GenerationConfig &config, Xoroshiro128PP rng -) - : config_(config), base_noise_(rng) { - base_noise_.calibrate( - config.base_scale, config.base_octaves, config.base_persistence - ); -} - -void BaseTileTypeGenerationPass::operator()(TileMap &tilemap) { - // Generate base tile types for each chunk - std::uint8_t map_size = tilemap.get_size(); - 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) { - generate_chunk(tilemap, chunk_x, chunk_y); - } - } -} - -void BaseTileTypeGenerationPass::generate_chunk( - TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y -) { - const Chunk &chunk = tilemap.get_chunk(chunk_x, chunk_y); - - // Generate each sub-chunk with its corresponding biome - 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); - BiomeType biome = chunk.get_biome(sub_pos); - generate_subchunk(tilemap, chunk_x, chunk_y, sub_pos, biome); - } - } -} - -void BaseTileTypeGenerationPass::generate_subchunk( - TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y, - SubChunkPos sub_pos, BiomeType biome -) { - const BiomeProperties &properties = get_biome_properties(biome); - - // Get starting tile coordinates for this sub-chunk - auto [start_x, start_y] = subchunk_to_tile_start(sub_pos); - - // Generate terrain for each tile in the sub-chunk - 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) { - // Calculate global coordinates - double global_x = chunk_x * Chunk::size + local_x; - 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); - - // Determine base terrain type - BaseTileType base_type - = determine_base_type(base_noise_value, properties); - - // Create tile with base and surface components - Tile tile; - tile.base = base_type; - tile.surface = SurfaceTileType::Empty; - - // Set the tile - TilePos pos{chunk_x, chunk_y, local_x, local_y}; - tilemap.set_tile(pos, tile); - } - } -} - -BaseTileType BaseTileTypeGenerationPass::determine_base_type( - double noise_value, const BiomeProperties &properties -) const { - const std::pair ratios[] = { - {BaseTileType::Water, properties.water_ratio}, - {BaseTileType::Ice, properties.ice_ratio }, - {BaseTileType::Sand, properties.sand_ratio }, - {BaseTileType::Land, properties.land_ratio }, - {BaseTileType::Mountain, 1.0 }, - }; - - for (const auto &[type, ratio] : ratios) { - if (noise_value < ratio) { - return type; - } - noise_value -= ratio; // Adjust noise value for next type - } - - std::unreachable(); -} - -HoleFillPass::HoleFillPass(const GenerationConfig &config): config_(config) {} - -void HoleFillPass::operator()(TileMap &tilemap) { - std::uint8_t map_size = tilemap.get_size(); - std::uint32_t total_tiles = map_size * Chunk::size; - - // Create visited array for the entire map - std::vector> visited( - total_tiles, std::vector(total_tiles, false) - ); - - // Process all tiles in the map - 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); - - // Only process passable tiles - if (!is_passable(tile.base)) { - visited[global_x][global_y] = true; - continue; - } - - // Find connected component - std::vector component_positions; - std::uint32_t component_size = bfs_component_size( - tilemap, pos, visited, component_positions - ); - - // Check if this component touches the boundary - bool touches_boundary = false; - for (const auto component_pos : component_positions) { - if (tilemap.is_at_boundary(component_pos)) { - touches_boundary = true; - break; - } - } - - // Fill small holes that don't touch the boundary - if (!touches_boundary - && 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; - tilemap.set_tile(fill_pos, fill_tile); - } - } - } - } - } - } -} - -bool HoleFillPass::is_passable(BaseTileType type) const { - return type != BaseTileType::Mountain; -} - -std::uint32_t HoleFillPass::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); - 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_passable(neighbor_tile.base)) { - visited[neighbor_global_x][neighbor_global_y] = true; - queue.push(neighbor); - } - } - } - - return size; -} - -SmoothenMountainsPass::SmoothenMountainsPass( - const GenerationConfig &config, Xoroshiro128PP rng -) - : config_(config), noise_(rng) {} - -void SmoothenMountainsPass::operator()(TileMap &tilemap) { - remove_small_mountain(tilemap); - for (int i = 1; i <= config_.mountain_smoothen_steps; ++i) { - smoothen_mountains(tilemap, i); - } - remove_small_mountain(tilemap); -} - -void SmoothenMountainsPass::remove_small_mountain(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 (tile.base != BaseTileType::Mountain) { - visited[global_x][global_y] = true; - continue; - } - - // Find connected component of mountains - 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, smooth it out - if (component_size <= config_.mountain_remove_threshold) { - demountainize(tilemap, component_positions); - } - } - } - } - } -} - -void SmoothenMountainsPass::demountainize( - TileMap &tilemap, const std::vector &pos -) { - // Step 1: Look around the mountain to see what should replace it - std::map type_count; - std::set unique_positions; - for (auto p : pos) { - auto neighbors = tilemap.get_neighbors(p, true); - 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> &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 (neighbor_tile.base == BaseTileType::Mountain) { - visited[neighbor_global_x][neighbor_global_y] = true; - queue.push(neighbor); - } - } - } - - return size; -} - -void SmoothenMountainsPass::smoothen_mountains( - TileMap &tilemap, std::uint32_t step_i -) { - struct CAConf { - int neighbor_count; - int fill_chance = 0; // n / 16 - int remove_chance = 0; // n / 16 - }; - - // Chance to fill or remove a mountain tile repects to the number of - // neighboring mountains (0 - 4) - constexpr CAConf cellularAutomataConfigurations[5] = { - {0, 0, 12}, - {1, 0, 4 }, - {2, 3, 1 }, - {3, 8, 0 }, - {4, 16, 0 } - }; - - 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 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(); - auto neighbors = tilemap.get_neighbors(pos); - - // Ignore if adjacent to the boundary - if (neighbors.size() < 4) { - continue; - } - - // Count neighboring mountains - int mountain_count = 0; - for (const auto &neighbor : neighbors) { - const Tile &tile = tilemap.get_tile(neighbor); - if (tile.base == BaseTileType::Mountain) { - mountain_count += 1; - } - } - - // Get the configuration for this count - const CAConf &conf - = cellularAutomataConfigurations[mountain_count]; - int rd = noise_.noise(global_x, global_y, step_i) & 0xF; - - Tile &tile = tilemap.get_tile(pos); - if (tile.base == BaseTileType::Mountain - && conf.remove_chance > rd) { - demountainize(tilemap, {pos}); - } else if (tile.base != BaseTileType::Mountain - && conf.fill_chance > rd) { - tile.base = BaseTileType::Mountain; - } - } - } - } - } -} - TerrainGenerator::TerrainGenerator(const GenerationConfig &config) : config_(config), master_rng_(config.seed) {} @@ -491,152 +9,10 @@ void TerrainGenerator::operator()(TileMap &tilemap) { biome_pass(tilemap); base_tile_type_pass(tilemap); smoothen_mountains_pass(tilemap); - hole_fill_pass(tilemap); + mountain_hole_fill_pass(tilemap); deepwater_pass(tilemap); } -void TerrainGenerator::biome_pass(TileMap &tilemap) { - // Create two RNGs for temperature and humidity noise - Xoroshiro128PP temp_rng = master_rng_; - master_rng_ = master_rng_.jump_96(); - Xoroshiro128PP humidity_rng = master_rng_; - master_rng_ = master_rng_.jump_96(); - - BiomeGenerationPass biome_pass(config_, temp_rng, humidity_rng); - biome_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); -} - -void TerrainGenerator::deepwater_pass(TileMap &tilemap) { - DeepwaterGenerationPass pass(config_.deepwater_radius); - pass(tilemap); -} - -DeepwaterGenerationPass::DeepwaterGenerationPass(std::uint32_t deepwater_radius) - : deepwater_radius_(deepwater_radius) {} - -void DeepwaterGenerationPass::operator()(TileMap &tilemap) { - std::uint8_t map_size = tilemap.get_size(); - - // Iterate through all sub-chunks to check biomes efficiently - 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) { - const Chunk &chunk = tilemap.get_chunk(chunk_x, chunk_y); - - // Process each sub-chunk - 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); - BiomeType biome = chunk.get_biome(sub_pos); - const BiomeProperties &properties - = get_biome_properties(biome); - - // Only process ocean biomes - if (!properties.is_ocean) { - continue; - } - - // Process all tiles in this ocean sub-chunk - process_ocean_subchunk(tilemap, chunk_x, chunk_y, sub_pos); - } - } - } - } -} - -void DeepwaterGenerationPass::process_ocean_subchunk( - TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y, - SubChunkPos sub_pos -) { - // Get starting tile coordinates for this sub-chunk - auto [start_x, start_y] = subchunk_to_tile_start(sub_pos); - - // Process all tiles in this sub-chunk - 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}; - - // Get the tile at this position - const Tile &tile = tilemap.get_tile(pos); - - // Only process water tiles - if (tile.base != BaseTileType::Water) { - continue; - } - - // Check if this water tile is surrounded by water/deepwater within - // the specified radius - if (is_surrounded_by_water(tilemap, pos, deepwater_radius_)) { - // Replace water with deepwater - Tile new_tile = tile; - new_tile.base = BaseTileType::Deepwater; - tilemap.set_tile(pos, new_tile); - } - } - } -} - -bool DeepwaterGenerationPass::is_surrounded_by_water( - const TileMap &tilemap, TilePos center_pos, std::uint32_t radius -) const { - auto [center_global_x, center_global_y] = center_pos.to_global(); - std::uint8_t map_size = tilemap.get_size(); - std::uint32_t max_global_coord = map_size * Chunk::size; - - // Check all tiles within the radius - for (std::int32_t dx = -static_cast(radius); - dx <= static_cast(radius); ++dx) { - for (std::int32_t dy = -static_cast(radius); - dy <= static_cast(radius); ++dy) { - std::int32_t check_x - = static_cast(center_global_x) + dx; - std::int32_t check_y - = static_cast(center_global_y) + dy; - - // Check bounds - if (check_x < 0 || check_y < 0 - || check_x >= static_cast(max_global_coord) - || check_y >= static_cast(max_global_coord)) { - return false; // Out of bounds, consider as non-water - } - - // Convert back to TilePos and check if it's water - TilePos check_pos = TilePos::from_global( - static_cast(check_x), - static_cast(check_y) - ); - - const Tile &check_tile = tilemap.get_tile(check_pos); - if (check_tile.base != BaseTileType::Water - && check_tile.base != BaseTileType::Deepwater) { - return false; // Found non-water tile within radius - } - } - } - - return true; // All tiles within radius are water or deepwater -} - void map_generate(TileMap &tilemap, const GenerationConfig &config) { TerrainGenerator generator(config); generator(tilemap); diff --git a/tilemap/src/pass/base_tile_type.cpp b/tilemap/src/pass/base_tile_type.cpp new file mode 100644 index 0000000..79480e7 --- /dev/null +++ b/tilemap/src/pass/base_tile_type.cpp @@ -0,0 +1,106 @@ +#include "biome.h" +#include "chunk.h" +#include "generation.h" + +namespace istd { + +BaseTileTypeGenerationPass::BaseTileTypeGenerationPass( + const GenerationConfig &config, Xoroshiro128PP rng +) + : config_(config), base_noise_(rng) { + base_noise_.calibrate( + config.base_scale, config.base_octaves, config.base_persistence + ); +} + +void BaseTileTypeGenerationPass::operator()(TileMap &tilemap) { + // Generate base tile types for each chunk + std::uint8_t map_size = tilemap.get_size(); + 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) { + generate_chunk(tilemap, chunk_x, chunk_y); + } + } +} + +void BaseTileTypeGenerationPass::generate_chunk( + TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y +) { + const Chunk &chunk = tilemap.get_chunk(chunk_x, chunk_y); + + // Generate each sub-chunk with its corresponding biome + 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); + BiomeType biome = chunk.get_biome(sub_pos); + generate_subchunk(tilemap, chunk_x, chunk_y, sub_pos, biome); + } + } +} + +void BaseTileTypeGenerationPass::generate_subchunk( + TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y, + SubChunkPos sub_pos, BiomeType biome +) { + const BiomeProperties &properties = get_biome_properties(biome); + + // Get starting tile coordinates for this sub-chunk + auto [start_x, start_y] = subchunk_to_tile_start(sub_pos); + + // Generate terrain for each tile in the sub-chunk + 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) { + // Calculate global coordinates + double global_x = chunk_x * Chunk::size + local_x; + 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); + + // Determine base terrain type + BaseTileType base_type + = determine_base_type(base_noise_value, properties); + + // Create tile with base and surface components + Tile tile; + tile.base = base_type; + tile.surface = SurfaceTileType::Empty; + + // Set the tile + TilePos pos{chunk_x, chunk_y, local_x, local_y}; + tilemap.set_tile(pos, tile); + } + } +} + +BaseTileType BaseTileTypeGenerationPass::determine_base_type( + double noise_value, const BiomeProperties &properties +) const { + const std::pair ratios[] = { + {BaseTileType::Water, properties.water_ratio}, + {BaseTileType::Ice, properties.ice_ratio }, + {BaseTileType::Sand, properties.sand_ratio }, + {BaseTileType::Land, properties.land_ratio }, + {BaseTileType::Mountain, 1.0 }, + }; + + for (const auto &[type, ratio] : ratios) { + if (noise_value < ratio) { + return type; + } + noise_value -= ratio; // Adjust noise value for next type + } + + std::unreachable(); +} + +void TerrainGenerator::base_tile_type_pass(TileMap &tilemap) { + BaseTileTypeGenerationPass pass(config_, master_rng_); + master_rng_ = master_rng_.jump_96(); + pass(tilemap); +} + +} // namespace istd \ No newline at end of file diff --git a/tilemap/src/pass/biome.cpp b/tilemap/src/pass/biome.cpp new file mode 100644 index 0000000..805bbd2 --- /dev/null +++ b/tilemap/src/pass/biome.cpp @@ -0,0 +1,82 @@ +#include "biome.h" +#include "generation.h" + +namespace istd { + +BiomeGenerationPass::BiomeGenerationPass( + const GenerationConfig &config, Xoroshiro128PP r1, Xoroshiro128PP r2 +) + : config_(config), temperature_noise_(r1), humidity_noise_(r2) { + temperature_noise_.calibrate( + config.temperature_scale, config.temperature_octaves, + config.temperature_persistence + ); + humidity_noise_.calibrate( + config.humidity_scale, config.humidity_octaves, + config.humidity_persistence + ); +} + +void BiomeGenerationPass::operator()(TileMap &tilemap) { + std::uint8_t map_size = tilemap.get_size(); + + // Generate biomes for each sub-chunk + 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) { + Chunk &chunk = tilemap.get_chunk(chunk_x, 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) { + // Calculate global position for this sub-chunk's center + auto [start_x, start_y] + = subchunk_to_tile_start(SubChunkPos(sub_x, sub_y)); + double global_x = chunk_x * Chunk::size + start_x + + Chunk::subchunk_size / 2; + + double global_y = chunk_y * Chunk::size + start_y + + Chunk::subchunk_size / 2; + + // Get climate values + auto [temperature, humidity] + = get_climate(global_x, global_y); + + // Determine biome and store directly in chunk + BiomeType biome = determine_biome(temperature, humidity); + chunk.biome[sub_x][sub_y] = biome; + } + } + } + } +} + +std::pair BiomeGenerationPass::get_climate( + double global_x, double global_y +) const { + // Generate temperature noise (0-1 range) + double temperature = temperature_noise_.uniform_noise( + global_x * config_.temperature_scale, + global_y * config_.temperature_scale + ); + + // Generate humidity noise (0-1 range) + double humidity = humidity_noise_.uniform_noise( + global_x * config_.humidity_scale, global_y * config_.humidity_scale + ); + + return {temperature, humidity}; +} + +void TerrainGenerator::biome_pass(TileMap &tilemap) { + // Create two RNGs for temperature and humidity noise + Xoroshiro128PP temp_rng = master_rng_; + master_rng_ = master_rng_.jump_96(); + Xoroshiro128PP humidity_rng = master_rng_; + master_rng_ = master_rng_.jump_96(); + + BiomeGenerationPass biome_pass(config_, temp_rng, humidity_rng); + biome_pass(tilemap); +} + +} // namespace istd diff --git a/tilemap/src/pass/deepwater.cpp b/tilemap/src/pass/deepwater.cpp new file mode 100644 index 0000000..bf5b5c9 --- /dev/null +++ b/tilemap/src/pass/deepwater.cpp @@ -0,0 +1,120 @@ +#include "biome.h" +#include "generation.h" + +namespace istd { + +DeepwaterGenerationPass::DeepwaterGenerationPass(std::uint32_t deepwater_radius) + : deepwater_radius_(deepwater_radius) {} + +void DeepwaterGenerationPass::operator()(TileMap &tilemap) { + std::uint8_t map_size = tilemap.get_size(); + + // Iterate through all sub-chunks to check biomes efficiently + 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) { + const Chunk &chunk = tilemap.get_chunk(chunk_x, chunk_y); + + // Process each sub-chunk + 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); + BiomeType biome = chunk.get_biome(sub_pos); + const BiomeProperties &properties + = get_biome_properties(biome); + + // Only process ocean biomes + if (!properties.is_ocean) { + continue; + } + + // Process all tiles in this ocean sub-chunk + process_ocean_subchunk(tilemap, chunk_x, chunk_y, sub_pos); + } + } + } + } +} + +void DeepwaterGenerationPass::process_ocean_subchunk( + TileMap &tilemap, std::uint8_t chunk_x, std::uint8_t chunk_y, + SubChunkPos sub_pos +) { + // Get starting tile coordinates for this sub-chunk + auto [start_x, start_y] = subchunk_to_tile_start(sub_pos); + + // Process all tiles in this sub-chunk + 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}; + + // Get the tile at this position + const Tile &tile = tilemap.get_tile(pos); + + // Only process water tiles + if (tile.base != BaseTileType::Water) { + continue; + } + + // Check if this water tile is surrounded by water/deepwater within + // the specified radius + if (is_surrounded_by_water(tilemap, pos, deepwater_radius_)) { + // Replace water with deepwater + Tile new_tile = tile; + new_tile.base = BaseTileType::Deepwater; + tilemap.set_tile(pos, new_tile); + } + } + } +} + +bool DeepwaterGenerationPass::is_surrounded_by_water( + const TileMap &tilemap, TilePos center_pos, std::uint32_t radius +) const { + auto [center_global_x, center_global_y] = center_pos.to_global(); + std::uint8_t map_size = tilemap.get_size(); + std::uint32_t max_global_coord = map_size * Chunk::size; + + // Check all tiles within the radius + for (std::int32_t dx = -static_cast(radius); + dx <= static_cast(radius); ++dx) { + for (std::int32_t dy = -static_cast(radius); + dy <= static_cast(radius); ++dy) { + std::int32_t check_x + = static_cast(center_global_x) + dx; + std::int32_t check_y + = static_cast(center_global_y) + dy; + + // Check bounds + if (check_x < 0 || check_y < 0 + || check_x >= static_cast(max_global_coord) + || check_y >= static_cast(max_global_coord)) { + return false; // Out of bounds, consider as non-water + } + + // Convert back to TilePos and check if it's water + TilePos check_pos = TilePos::from_global( + static_cast(check_x), + static_cast(check_y) + ); + + const Tile &check_tile = tilemap.get_tile(check_pos); + if (check_tile.base != BaseTileType::Water + && check_tile.base != BaseTileType::Deepwater) { + return false; // Found non-water tile within radius + } + } + } + + return true; // All tiles within radius are water or deepwater +} + +void TerrainGenerator::deepwater_pass(TileMap &tilemap) { + DeepwaterGenerationPass pass(config_.deepwater_radius); + pass(tilemap); +} + +} // namespace istd \ No newline at end of file diff --git a/tilemap/src/pass/mountain_hole_fill.cpp b/tilemap/src/pass/mountain_hole_fill.cpp new file mode 100644 index 0000000..420c7e6 --- /dev/null +++ b/tilemap/src/pass/mountain_hole_fill.cpp @@ -0,0 +1,117 @@ +#include "generation.h" +#include + +namespace istd { + +MountainHoleFillPass::MountainHoleFillPass(const GenerationConfig &config) + : config_(config) {} + +void MountainHoleFillPass::operator()(TileMap &tilemap) { + std::uint8_t map_size = tilemap.get_size(); + std::uint32_t total_tiles = map_size * Chunk::size; + + // Create visited array for the entire map + std::vector> visited( + total_tiles, std::vector(total_tiles, false) + ); + + // Process all tiles in the map + 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); + + // Only process passable tiles + if (!is_passable(tile.base)) { + visited[global_x][global_y] = true; + continue; + } + + // Find connected component + std::vector component_positions; + std::uint32_t component_size = bfs_component_size( + tilemap, pos, visited, component_positions + ); + + // Check if this component touches the boundary + bool touches_boundary = false; + for (const auto component_pos : component_positions) { + if (tilemap.is_at_boundary(component_pos)) { + touches_boundary = true; + break; + } + } + + // Fill small holes that don't touch the boundary + if (!touches_boundary + && 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; + tilemap.set_tile(fill_pos, fill_tile); + } + } + } + } + } + } +} + +bool MountainHoleFillPass::is_passable(BaseTileType type) const { + return type != BaseTileType::Mountain; +} + +std::uint32_t MountainHoleFillPass::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); + 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_passable(neighbor_tile.base)) { + visited[neighbor_global_x][neighbor_global_y] = true; + queue.push(neighbor); + } + } + } + + return size; +} + +void TerrainGenerator::mountain_hole_fill_pass(TileMap &tilemap) { + MountainHoleFillPass pass(config_); + pass(tilemap); +} + +} // namespace istd \ No newline at end of file diff --git a/tilemap/src/pass/smoothen_mountain.cpp b/tilemap/src/pass/smoothen_mountain.cpp new file mode 100644 index 0000000..78e5034 --- /dev/null +++ b/tilemap/src/pass/smoothen_mountain.cpp @@ -0,0 +1,227 @@ +#include "generation.h" +#include +#include +#include + +namespace istd { + +SmoothenMountainsPass::SmoothenMountainsPass( + const GenerationConfig &config, Xoroshiro128PP rng +) + : config_(config), noise_(rng) {} + +void SmoothenMountainsPass::operator()(TileMap &tilemap) { + remove_small_mountain(tilemap); + for (int i = 1; i <= config_.mountain_smoothen_steps; ++i) { + smoothen_mountains(tilemap, i); + } + remove_small_mountain(tilemap); +} + +void SmoothenMountainsPass::remove_small_mountain(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 (tile.base != BaseTileType::Mountain) { + visited[global_x][global_y] = true; + continue; + } + + // Find connected component of mountains + 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, smooth it out + if (component_size <= config_.mountain_remove_threshold) { + demountainize(tilemap, component_positions); + } + } + } + } + } +} + +void SmoothenMountainsPass::demountainize( + TileMap &tilemap, const std::vector &pos +) { + // Step 1: Look around the mountain to see what should replace it + std::map type_count; + std::set unique_positions; + for (auto p : pos) { + auto neighbors = tilemap.get_neighbors(p, true); + 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> &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 (neighbor_tile.base == BaseTileType::Mountain) { + visited[neighbor_global_x][neighbor_global_y] = true; + queue.push(neighbor); + } + } + } + + return size; +} + +void SmoothenMountainsPass::smoothen_mountains( + TileMap &tilemap, std::uint32_t step_i +) { + struct CAConf { + int neighbor_count; + int fill_chance = 0; // n / 16 + int remove_chance = 0; // n / 16 + }; + + // Chance to fill or remove a mountain tile repects to the number of + // neighboring mountains (0 - 4) + constexpr CAConf cellularAutomataConfigurations[5] = { + {0, 0, 12}, + {1, 0, 4 }, + {2, 3, 1 }, + {3, 8, 0 }, + {4, 16, 0 } + }; + + 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 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(); + auto neighbors = tilemap.get_neighbors(pos); + + // Ignore if adjacent to the boundary + if (neighbors.size() < 4) { + continue; + } + + // Count neighboring mountains + int mountain_count = 0; + for (const auto &neighbor : neighbors) { + const Tile &tile = tilemap.get_tile(neighbor); + if (tile.base == BaseTileType::Mountain) { + mountain_count += 1; + } + } + + // Get the configuration for this count + const CAConf &conf + = cellularAutomataConfigurations[mountain_count]; + int rd = noise_.noise(global_x, global_y, step_i) & 0xF; + + Tile &tile = tilemap.get_tile(pos); + if (tile.base == BaseTileType::Mountain + && conf.remove_chance > rd) { + demountainize(tilemap, {pos}); + } else if (tile.base != BaseTileType::Mountain + && conf.fill_chance > rd) { + tile.base = BaseTileType::Mountain; + } + } + } + } + } +} + +void TerrainGenerator::smoothen_mountains_pass(TileMap &tilemap) { + SmoothenMountainsPass pass(config_, master_rng_); + master_rng_ = master_rng_.jump_96(); + pass(tilemap); +} + +} // namespace istd \ No newline at end of file