feat: add oil generation pass and update related configurations and documentation

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2025-08-03 14:57:36 +08:00
parent 6a3f23f989
commit 937a1324eb
Signed by: szTom
GPG Key ID: 072D999D60C6473C
15 changed files with 516 additions and 44 deletions

View File

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

View File

@ -51,10 +51,10 @@ struct Tile {
``` ```
**Base Tile Types:** **Base Tile Types:**
- `Land`, `Mountain`, `Sand`, `Water`, `Ice` - `Land`, `Mountain`, `Sand`, `Water`, `Ice`, `Deepwater`
**Surface Tile Types:** **Surface Tile Types:**
- `Empty`, `Wood`, `Structure` - `Empty`, `Oil`
### Position Types ### Position Types
@ -90,6 +90,12 @@ struct GenerationConfig {
int temperature_octaves = 3; int temperature_octaves = 3;
int humidity_octaves = 3; int humidity_octaves = 3;
int base_octaves = 3; int base_octaves = 3;
// Oil generation parameters
double oil_density = 0.8; // Average oil fields per chunk
std::uint32_t oil_cluster_min_size = 2; // Minimum tiles per cluster
std::uint32_t oil_cluster_max_size = 6; // Maximum tiles per cluster
double oil_biome_preference = 2.0; // Multiplier for preferred biomes
}; };
``` ```

View File

@ -27,8 +27,10 @@ tilemap/
│ ├── biome.cpp # Climate-based biome generation │ ├── biome.cpp # Climate-based biome generation
│ ├── base_tile_type.cpp # Base terrain generation │ ├── base_tile_type.cpp # Base terrain generation
│ ├── smoothen_mountain.cpp # Mountain smoothing │ ├── smoothen_mountain.cpp # Mountain smoothing
│ ├── smoothen_island.cpp # Island smoothing
│ ├── mountain_hole_fill.cpp # Hole filling │ ├── mountain_hole_fill.cpp # Hole filling
│ └── deepwater.cpp # Deep water placement │ ├── deepwater.cpp # Deep water placement
│ └── oil.cpp # Oil resource generation
├── examples/ # Usage examples ├── examples/ # Usage examples
└── docs/ # Documentation └── docs/ # Documentation
``` ```
@ -51,8 +53,10 @@ Terrain generation uses a multi-pass pipeline for modularity and control:
1. **Biome Pass**: Generates climate data and assigns biomes to sub-chunks 1. **Biome Pass**: Generates climate data and assigns biomes to sub-chunks
2. **Base Tile Type Pass**: Creates base terrain based on biomes 2. **Base Tile Type Pass**: Creates base terrain based on biomes
3. **Mountain Smoothing Pass**: Removes isolated mountain clusters 3. **Mountain Smoothing Pass**: Removes isolated mountain clusters
4. **Hole Fill Pass**: Fills small terrain holes 4. **Island Smoothing Pass**: Smooths island coastlines using cellular automata
5. **Deep Water Pass**: Places deep water areas 5. **Hole Fill Pass**: Fills small terrain holes
6. **Deep Water Pass**: Places deep water areas
7. **Oil Pass**: Generates sparse oil deposits as surface features
Each pass operates independently with its own RNG state, ensuring deterministic results. Each pass operates independently with its own RNG state, ensuring deterministic results.
@ -83,9 +87,18 @@ The library addresses Perlin noise distribution issues:
Several passes use BFS (Breadth-First Search) for terrain analysis: Several passes use BFS (Breadth-First Search) for terrain analysis:
- **Mountain Smoothing**: Find and remove small mountain components - **Mountain Smoothing**: Find and remove small mountain components
- **Island Smoothing**: Apply cellular automata for natural coastlines
- **Hole Filling**: Identify and fill isolated terrain holes - **Hole Filling**: Identify and fill isolated terrain holes
- Components touching map boundaries are preserved - Components touching map boundaries are preserved
### Oil Resource Generation
The oil generation pass creates sparse resource deposits:
- **Poisson Disk Sampling**: Ensures minimum distance between oil fields
- **Biome Preference**: Higher probability in desert and plains biomes
- **Cluster Growth**: Random walk creates 2-6 tile clusters
- **Surface Placement**: Oil appears as surface features on land/sand tiles
## Random Number Generation ## Random Number Generation
### Xoroshiro128++ Implementation ### Xoroshiro128++ Implementation

View File

@ -5,13 +5,18 @@
#include <chrono> #include <chrono>
#include <cstdlib> #include <cstdlib>
#include <format> #include <format>
#include <iostream>
#include <print> #include <print>
#include <string> #include <string>
// Get BMP color for different base tile types // Get BMP color for different tile types, considering surface tiles
BmpColors::Color get_tile_color(istd::BaseTileType type) { BmpColors::Color get_tile_color(const istd::Tile &tile) {
switch (type) { // Oil surface tile overrides base color
if (tile.surface == istd::SurfaceTileType::Oil) {
return BmpColors::OIL;
}
// Otherwise use base tile color
switch (tile.base) {
case istd::BaseTileType::Land: case istd::BaseTileType::Land:
return BmpColors::LAND; return BmpColors::LAND;
case istd::BaseTileType::Mountain: case istd::BaseTileType::Mountain:
@ -50,7 +55,7 @@ void generate_bmp(const istd::TileMap &tilemap, const std::string &filename) {
int global_x = chunk_x * tiles_per_chunk + tile_x; int global_x = chunk_x * tiles_per_chunk + tile_x;
int global_y = chunk_y * tiles_per_chunk + tile_y; int global_y = chunk_y * tiles_per_chunk + tile_y;
auto color = get_tile_color(tile.base); auto color = get_tile_color(tile);
// Draw a tile_size x tile_size block // Draw a tile_size x tile_size block
for (int dy = 0; dy < tile_size; ++dy) { for (int dy = 0; dy < tile_size; ++dy) {
@ -83,6 +88,7 @@ void print_statistics(const istd::TileMap &tilemap) {
int tile_counts[6] = { int tile_counts[6] = {
0 0
}; // Count for each base tile type (now 6 types including Deepwater) }; // Count for each base tile type (now 6 types including Deepwater)
int oil_count = 0; // Count oil surface tiles
const int chunks_per_side = tilemap.get_size(); const int chunks_per_side = tilemap.get_size();
const int tiles_per_chunk = istd::Chunk::size; const int tiles_per_chunk = istd::Chunk::size;
@ -94,15 +100,20 @@ void print_statistics(const istd::TileMap &tilemap) {
for (int tile_y = 0; tile_y < tiles_per_chunk; ++tile_y) { for (int tile_y = 0; tile_y < tiles_per_chunk; ++tile_y) {
const auto &tile = chunk.tiles[tile_x][tile_y]; const auto &tile = chunk.tiles[tile_x][tile_y];
tile_counts[static_cast<int>(tile.base)]++; tile_counts[static_cast<int>(tile.base)]++;
// Count oil surface tiles
if (tile.surface == istd::SurfaceTileType::Oil) {
oil_count++;
}
} }
} }
} }
} }
const char *tile_names[] const char *tile_names[] = {"Land", "Mountain", "Sand",
= {"Land", "Mountain", "Sand", "Water", "Ice", "Deepwater"}; "Water", "Ice", "Deepwater"};
int total_tiles int total_tiles = chunks_per_side * chunks_per_side * tiles_per_chunk
= chunks_per_side * chunks_per_side * tiles_per_chunk * tiles_per_chunk; * tiles_per_chunk;
std::println("\nTile Statistics:"); std::println("\nTile Statistics:");
std::println("================"); std::println("================");
@ -112,6 +123,16 @@ void print_statistics(const istd::TileMap &tilemap) {
"{:>10}: {:>8} ({:.1f}%)", tile_names[i], tile_counts[i], percentage "{:>10}: {:>8} ({:.1f}%)", tile_names[i], tile_counts[i], percentage
); );
} }
// Print oil statistics
double oil_percentage = (double)oil_count / total_tiles * 100.0;
double oil_per_chunk = (double)oil_count
/ (chunks_per_side * chunks_per_side);
std::println(
"{:>10}: {:>8} ({:.1f}%, {:.2f} per chunk)", "Oil", oil_count,
oil_percentage, oil_per_chunk
);
std::println("Total tiles: {}", total_tiles); std::println("Total tiles: {}", total_tiles);
} }
@ -131,8 +152,9 @@ int main(int argc, char *argv[]) {
return 1; return 1;
} }
istd::Seed seed istd::Seed seed = istd::Seed::from_string(
= istd::Seed::from_string(argc >= 2 ? argv[1] : "hello_world"); 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 = 8; // Default value int chunks_per_side = 8; // Default value

View File

@ -235,6 +235,7 @@ constexpr Color SAND(244, 164, 96); // Sandy brown
constexpr Color WATER(30, 144, 255); // Dodger blue constexpr Color WATER(30, 144, 255); // Dodger blue
constexpr Color ICE(176, 224, 230); // Powder blue constexpr Color ICE(176, 224, 230); // Powder blue
constexpr Color DEEPWATER(0, 0, 139); // Dark blue constexpr Color DEEPWATER(0, 0, 139); // Dark blue
constexpr Color OIL(0, 0, 0); // Black
} // namespace BmpColors } // namespace BmpColors
#endif // BMP_H #endif // BMP_H

View File

@ -3,6 +3,8 @@
#include "tile.h" #include "tile.h"
#include <compare> #include <compare>
#include <cstdint> #include <cstdint>
#include <functional>
#include <utility>
namespace istd { namespace istd {
@ -27,6 +29,13 @@ struct TilePos {
uint8_t local_x; uint8_t local_x;
uint8_t local_y; uint8_t local_y;
/**
* @brief Calculate squared distance to another TilePos
* @param other Other TilePos to compare with
* @return Squared distance between the two positions
*/
std::uint32_t sqr_distance_to(TilePos other) const;
/** /**
* @brief Convert TilePos to global coordinates * @brief Convert TilePos to global coordinates
* @return Pair of global X and Y coordinates * @return Pair of global X and Y coordinates
@ -40,15 +49,23 @@ struct TilePos {
* @return TilePos corresponding to the global coordinates * @return TilePos corresponding to the global coordinates
*/ */
static TilePos from_global(std::uint16_t global_x, std::uint16_t global_y); static TilePos from_global(std::uint16_t global_x, std::uint16_t global_y);
};
/** /**
* @brief Three-way comparison operator for TilePos * @brief Three-way comparison operator for TilePos
* @param lhs Left-hand side TilePos * @param lhs Left-hand side TilePos
* @param rhs Right-hand side TilePos * @param rhs Right-hand side TilePos
* @return Strong ordering comparison result * @return Strong ordering comparison result
*/ */
std::strong_ordering operator<=>(const TilePos &lhs, const TilePos &rhs); friend std::strong_ordering operator<=>(TilePos lhs, TilePos rhs);
/**
* @brief Equality comparison operator for TilePos
* @param lhs Left-hand side TilePos
* @param rhs Right-hand side TilePos
* @return True if positions are equal
*/
friend bool operator==(TilePos lhs, TilePos rhs) = default;
};
struct Chunk { struct Chunk {
// Size of a chunk in tiles (64 x 64) // Size of a chunk in tiles (64 x 64)
@ -83,6 +100,28 @@ struct Chunk {
const BiomeType &get_biome(SubChunkPos pos) const { const BiomeType &get_biome(SubChunkPos pos) const {
return biome[pos.sub_x][pos.sub_y]; return biome[pos.sub_x][pos.sub_y];
} }
/**
* @brief Get biome for a specific local tile position
* @param local_x Local X coordinate within the chunk
* @param local_y Local Y coordinate within the chunk
* @return Reference to biome type
*/
const BiomeType &get_biome(
std::uint8_t local_x, std::uint8_t local_y
) const {
SubChunkPos sub_pos(local_x / subchunk_size, local_y / subchunk_size);
return get_biome(sub_pos);
}
/**
* @brief Get biome for a specific TilePos
* @param pos TilePos to get the biome for
* @return Reference to biome type
*/
const BiomeType &get_biome(TilePos pos) const {
return get_biome(pos.local_x, pos.local_y);
}
}; };
/** /**
@ -94,4 +133,14 @@ std::pair<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos);
} // namespace istd } // namespace istd
template<>
struct std::hash<istd::TilePos> {
::std::size_t operator()(istd::TilePos pos) const {
return (static_cast<::std::size_t>(pos.chunk_x) << 24)
| (static_cast<::std::size_t>(pos.chunk_y) << 16)
| (static_cast<::std::size_t>(pos.local_x) << 8)
| static_cast<::std::size_t>(pos.local_y);
}
};
#endif #endif

View File

@ -5,6 +5,7 @@
#include "chunk.h" #include "chunk.h"
#include "noise.h" #include "noise.h"
#include "tilemap.h" #include "tilemap.h"
#include "xoroshiro.h"
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
@ -40,6 +41,14 @@ struct GenerationConfig {
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
// Oil generation parameters
std::uint8_t oil_density = 204; // Average oil fields per 255 chunk (~0.8)
std::uint8_t oil_cluster_min_size = 1; // Minimum tiles per oil cluster
std::uint8_t oil_cluster_max_size = 7; // Maximum tiles per oil cluster
// (should be <= 24)
std::uint8_t oil_base_probe = 128; // Biome preference multiplier (out
// of 255)
}; };
class BiomeGenerationPass { class BiomeGenerationPass {
@ -351,6 +360,12 @@ private:
* @param tilemap The tilemap to process * @param tilemap The tilemap to process
*/ */
void deepwater_pass(TileMap &tilemap); void deepwater_pass(TileMap &tilemap);
/**
* @brief Generate oil clusters on suitable terrain
* @param tilemap The tilemap to process
*/
void oil_pass(TileMap &tilemap);
}; };
class DeepwaterGenerationPass { class DeepwaterGenerationPass {
@ -407,6 +422,67 @@ private:
) const; ) const;
}; };
class OilGenerationPass {
private:
const GenerationConfig &config_;
Xoroshiro128PP rng_;
DiscreteRandomNoise noise_;
public:
/**
* @brief Construct an oil generation pass
* @param config Generation configuration parameters
* @param rng Random number generator for oil placement
* @param noise_rng Random number generator for noise-based operations
*/
OilGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng,
Xoroshiro128PP noise_rng
);
/**
* @brief Generate oil clusters on the tilemap
* @param tilemap The tilemap to process
*/
void operator()(TileMap &tilemap);
private:
/**
* @brief Generate oil center positions using Poisson disk sampling
* @param tilemap The tilemap to analyze
* @return Vector of positions where oil clusters should be placed
*/
std::vector<TilePos> generate_oil_centers(const TileMap &tilemap);
/**
* @brief Generate an oil cluster around a center position
* @param tilemap The tilemap to modify
* @param center Center position for the oil cluster
*/
void generate_oil_cluster(TileMap &tilemap, TilePos center);
/**
* @brief Check if a tile is suitable for oil placement
* @param tilemap The tilemap to check
* @param pos Position to check
* @return True if oil can be placed at this position
*/
bool is_suitable_for_oil(const TileMap &tilemap, TilePos pos) const;
/**
* @brief Get biome preference multiplier for oil generation (out of 255)
* @param biome The biome type to check
* @return Preference value (0-255)
*/
std::uint8_t get_biome_oil_preference(BiomeType biome) const;
/**
* @brief Calculate minimum distance between oil fields based on map size
* @return Minimum distance in tiles
*/
std::uint32_t calculate_min_oil_distance() const;
};
/** /**
* @brief Generate a tilemap using the new biome-based system * @brief Generate a tilemap using the new biome-based system
* @param tilemap The tilemap to generate into * @param tilemap The tilemap to generate into

View File

@ -19,28 +19,55 @@ private:
std::uint64_t mask; std::uint64_t mask;
std::array<std::uint8_t, 256> permutation_; std::array<std::uint8_t, 256> permutation_;
std::uint8_t perm(int x) const; std::uint8_t perm(int x) const noexcept;
std::uint32_t map(std::uint32_t x) const; std::uint32_t rot8(std::uint32_t x) const noexcept;
std::uint32_t map_once(std::uint32_t x) const noexcept;
std::uint32_t map(std::uint32_t x) const noexcept;
public: public:
/** /**
* @brief Construct a DiscreteRandomNoise generator with the given seed * @brief Construct a DiscreteRandomNoise generator with the given seed
* @param rng Random number generator for noise * @param rng Random number generator for noise
*/ */
explicit DiscreteRandomNoise(Xoroshiro128PP rng); explicit DiscreteRandomNoise(Xoroshiro128PP rng) noexcept;
DiscreteRandomNoise() = default;
/** /**
* @brief Generate a discrete random value at the given coordinates * @brief Generate a discrete random value at the given coordinates
* @param x X coordinate * @param x X coordinate
* @param y Y coordinate * @param y Y coordinate
* @param z Z coordinate (optional) * @param z Z coordinate (optional)
* @return Discrete random value between 0 and 255 * @return Discrete random value between 0 and 2^64-1
*/ */
std::uint64_t noise( std::uint64_t noise(
std::uint32_t x, std::uint32_t y, std::uint32_t z = 0 std::uint32_t x, std::uint32_t y, std::uint32_t z = 0
) const; ) const noexcept;
};
class DiscreteRandomNoiseStream {
private:
const DiscreteRandomNoise &noise_;
std::uint32_t x_;
std::uint32_t y_;
std::uint32_t idx_;
public:
DiscreteRandomNoiseStream(
const DiscreteRandomNoise &noise, std::uint32_t x, std::uint32_t y,
std::uint32_t idx = 0
);
std::uint64_t next() noexcept;
// Adaption for STL RandomEngine named requirements
using result_type = std::uint64_t;
static constexpr result_type min() noexcept {
return std::numeric_limits<result_type>::min();
}
static constexpr result_type max() noexcept {
return std::numeric_limits<result_type>::max();
}
// Equivalent to next(), for STL compatibility
result_type operator()() noexcept;
}; };
class PerlinNoise { class PerlinNoise {

View File

@ -2,7 +2,6 @@
#define ISTD_TILEMAP_TILE_H #define ISTD_TILEMAP_TILE_H
#include <cstdint> #include <cstdint>
#include <stdexcept> // For std::invalid_argument
namespace istd { namespace istd {
@ -18,14 +17,17 @@ enum class BaseTileType : std::uint8_t {
enum class SurfaceTileType : std::uint8_t { enum class SurfaceTileType : std::uint8_t {
Empty, Empty,
Oil,
_count _count
}; };
constexpr std::uint8_t base_tile_count constexpr std::uint8_t base_tile_count = static_cast<std::uint8_t>(
= static_cast<std::uint8_t>(BaseTileType::_count); BaseTileType::_count
);
constexpr std::uint8_t surface_tile_count constexpr std::uint8_t surface_tile_count = static_cast<std::uint8_t>(
= static_cast<std::uint8_t>(SurfaceTileType::_count); SurfaceTileType::_count
);
static_assert(base_tile_count <= 16, "Base tile don't fit in 4 bits"); static_assert(base_tile_count <= 16, "Base tile don't fit in 4 bits");
static_assert(surface_tile_count <= 16, "Surface tile don't fit in 4 bits"); static_assert(surface_tile_count <= 16, "Surface tile don't fit in 4 bits");

View File

@ -34,6 +34,13 @@ public:
Chunk &get_chunk(std::uint8_t chunk_x, std::uint8_t chunk_y); Chunk &get_chunk(std::uint8_t chunk_x, std::uint8_t chunk_y);
const Chunk &get_chunk(std::uint8_t chunk_x, std::uint8_t chunk_y) const; const Chunk &get_chunk(std::uint8_t chunk_x, std::uint8_t chunk_y) const;
/**
* @brief Get a reference to the chunk containing the given TilePos
* @param pos The TilePos to get the chunk for
*/
Chunk &get_chunk_of(TilePos pos);
const Chunk &get_chunk_of(TilePos pos) const;
/** /**
* @brief Get a tile at the given position * @brief Get a tile at the given position
* @param pos The position of the tile * @param pos The position of the tile

View File

@ -1,8 +1,9 @@
#include "chunk.h" #include "chunk.h"
#include <cstdint>
namespace istd { namespace istd {
std::strong_ordering operator<=>(const TilePos &lhs, const TilePos &rhs) { std::strong_ordering operator<=>(TilePos lhs, TilePos rhs) {
if (lhs.chunk_x != rhs.chunk_x) { if (lhs.chunk_x != rhs.chunk_x) {
return lhs.chunk_x <=> rhs.chunk_x; return lhs.chunk_x <=> rhs.chunk_x;
} }
@ -23,6 +24,24 @@ std::pair<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos) {
return {pos.sub_x * Chunk::subchunk_size, pos.sub_y * Chunk::subchunk_size}; return {pos.sub_x * Chunk::subchunk_size, pos.sub_y * Chunk::subchunk_size};
} }
std::uint32_t TilePos::sqr_distance_to(TilePos other) const {
auto [this_global_x, this_global_y] = to_global();
auto [other_global_x, other_global_y] = other.to_global();
using std::swap;
if (this_global_x < other_global_x) {
swap(this_global_x, other_global_x);
}
if (this_global_y < other_global_y) {
swap(this_global_y, other_global_y);
}
std::uint32_t dx = this_global_x - other_global_x;
std::uint32_t dy = this_global_y - other_global_y;
return dx * dx + dy * dy;
}
std::pair<std::uint16_t, std::uint16_t> TilePos::to_global() const { 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}; return {chunk_x * Chunk::size + local_x, chunk_y * Chunk::size + local_y};
} }

View File

@ -11,6 +11,7 @@ void TerrainGenerator::operator()(TileMap &tilemap) {
smoothen_islands_pass(tilemap); smoothen_islands_pass(tilemap);
mountain_hole_fill_pass(tilemap); mountain_hole_fill_pass(tilemap);
deepwater_pass(tilemap); deepwater_pass(tilemap);
oil_pass(tilemap);
} }
void map_generate(TileMap &tilemap, const GenerationConfig &config) { void map_generate(TileMap &tilemap, const GenerationConfig &config) {

View File

@ -1,5 +1,6 @@
#include "noise.h" #include "noise.h"
#include <algorithm> #include <algorithm>
#include <bit>
#include <cmath> #include <cmath>
#include <numeric> #include <numeric>
#include <random> #include <random>
@ -7,19 +8,23 @@
namespace istd { namespace istd {
DiscreteRandomNoise::DiscreteRandomNoise(Xoroshiro128PP rng) { DiscreteRandomNoise::DiscreteRandomNoise(Xoroshiro128PP rng) noexcept {
mask = rng.next(); mask = rng.next();
std::iota(permutation_.begin(), permutation_.end(), 0); std::iota(permutation_.begin(), permutation_.end(), 0);
std::shuffle(permutation_.begin(), permutation_.end(), rng); std::shuffle(permutation_.begin(), permutation_.end(), rng);
} }
std::uint8_t DiscreteRandomNoise::perm(int x) const { std::uint8_t DiscreteRandomNoise::perm(int x) const noexcept {
// Map x to [0, 255] range // Map x to [0, 255] range
x &= 0xFF; x &= 0xFF;
return permutation_[x]; return permutation_[x];
} }
std::uint32_t DiscreteRandomNoise::map(std::uint32_t x) const { std::uint32_t DiscreteRandomNoise::rot8(std::uint32_t x) const noexcept {
return std::rotl(x, 8);
}
std::uint32_t DiscreteRandomNoise::map_once(std::uint32_t x) const noexcept {
std::uint8_t a = x & 0xFF; std::uint8_t a = x & 0xFF;
std::uint8_t b = (x >> 8) & 0xFF; std::uint8_t b = (x >> 8) & 0xFF;
std::uint8_t c = (x >> 16) & 0xFF; std::uint8_t c = (x >> 16) & 0xFF;
@ -31,9 +36,17 @@ std::uint32_t DiscreteRandomNoise::map(std::uint32_t x) const {
return (d << 24U) | (c << 16U) | (b << 8U) | a; return (d << 24U) | (c << 16U) | (b << 8U) | a;
} }
std::uint32_t DiscreteRandomNoise::map(std::uint32_t x) const noexcept {
for (int i = 0; i < 3; ++i) {
x = map_once(x);
x = rot8(x);
}
return x;
}
std::uint64_t DiscreteRandomNoise::noise( std::uint64_t DiscreteRandomNoise::noise(
std::uint32_t x, std::uint32_t y, std::uint32_t z std::uint32_t x, std::uint32_t y, std::uint32_t z
) const { ) const noexcept {
auto A = map(x); auto A = map(x);
auto B = map(y ^ A); auto B = map(y ^ A);
auto C = map(z ^ B); auto C = map(z ^ B);
@ -43,6 +56,22 @@ std::uint64_t DiscreteRandomNoise::noise(
return ((static_cast<std::uint64_t>(C) << 32) | F) ^ mask; return ((static_cast<std::uint64_t>(C) << 32) | F) ^ mask;
} }
DiscreteRandomNoiseStream::DiscreteRandomNoiseStream(
const DiscreteRandomNoise &noise, std::uint32_t x, std::uint32_t y,
std::uint32_t idx
)
: noise_(noise), x_(x), y_(y), idx_(idx) {}
std::uint64_t DiscreteRandomNoiseStream::next() noexcept {
std::uint64_t value = noise_.noise(x_, y_, idx_);
++idx_;
return value;
}
std::uint64_t DiscreteRandomNoiseStream::operator()() noexcept {
return next();
}
PerlinNoise::PerlinNoise(Xoroshiro128PP rng) { PerlinNoise::PerlinNoise(Xoroshiro128PP rng) {
// Initialize permutation array with values 0-255 // Initialize permutation array with values 0-255
permutation_.resize(256); permutation_.resize(256);
@ -191,8 +220,9 @@ double UniformPerlinNoise::uniform_noise(double x, double y) const {
double UniformPerlinNoise::map_to_uniform(double raw_value) const { double UniformPerlinNoise::map_to_uniform(double raw_value) const {
// Find position in CDF using binary search // Find position in CDF using binary search
auto it auto it = std::lower_bound(
= std::lower_bound(cdf_values_.begin(), cdf_values_.end(), raw_value); cdf_values_.begin(), cdf_values_.end(), raw_value
);
// Calculate quantile (position in CDF as fraction) // Calculate quantile (position in CDF as fraction)
size_t position = std::distance(cdf_values_.begin(), it); size_t position = std::distance(cdf_values_.begin(), it);

209
tilemap/src/pass/oil.cpp Normal file
View File

@ -0,0 +1,209 @@
#include "biome.h"
#include "chunk.h"
#include "generation.h"
#include "noise.h"
#include "xoroshiro.h"
#include <algorithm>
#include <queue>
#include <random>
#include <unordered_set>
namespace istd {
OilGenerationPass::OilGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng, Xoroshiro128PP noise_rng
)
: config_(config), rng_(rng), noise_(noise_rng) {}
void OilGenerationPass::operator()(TileMap &tilemap) {
// Generate oil center positions using Poisson disk sampling
auto oil_centers = generate_oil_centers(tilemap);
// Generate oil clusters around each center
for (const auto &center : oil_centers) {
generate_oil_cluster(tilemap, center);
}
}
std::vector<TilePos> OilGenerationPass::generate_oil_centers(
const TileMap &tilemap
) {
std::vector<TilePos> centers;
std::uint8_t map_size = tilemap.get_size();
std::uint32_t total_chunks = map_size * map_size;
// Calculate expected number of oil fields based on density (out of 255)
std::uint32_t expected_oil_fields = (total_chunks * config_.oil_density)
/ 255;
// Minimum distance between oil fields to ensure spacing
std::uint32_t min_distance = calculate_min_oil_distance();
// Use Poisson disk sampling approach
const std::uint32_t max_coord = map_size * Chunk::size - 1;
// Generate candidates with rejection sampling
// Avoid infinite loops
std::uint32_t attempts = 0;
const std::uint32_t max_attempts = expected_oil_fields * 32;
std::uniform_int_distribution<std::uint16_t> dist(0, max_coord);
while (centers.size() < expected_oil_fields && attempts < max_attempts) {
++attempts;
// Generate random position using xoroshiro
const auto global_x = dist(rng_);
const auto global_y = dist(rng_);
TilePos candidate = TilePos::from_global(global_x, global_y);
// Check if position is suitable for oil
if (!is_suitable_for_oil(tilemap, candidate)) {
continue;
}
// Check distance to existing oil centers
auto distance_checker = [candidate, min_distance](TilePos existing) {
return candidate.sqr_distance_to(existing)
< (min_distance * min_distance);
};
if (std::ranges::any_of(centers, distance_checker)) {
continue;
}
// Apply biome preference
const Chunk &chunk = tilemap.get_chunk_of(candidate);
BiomeType biome = chunk.get_biome(candidate);
std::uint8_t biome_preference = get_biome_oil_preference(biome);
// Use integer probability check (0-255)
std::uint8_t sample = noise_.noise(global_x, global_y);
if (sample < biome_preference) {
centers.push_back(candidate);
}
}
return centers;
}
void OilGenerationPass::generate_oil_cluster(TileMap &tilemap, TilePos center) {
auto [global_x, global_y] = center.to_global();
auto span = config_.oil_cluster_max_size - config_.oil_cluster_min_size;
auto cluster_size = config_.oil_cluster_min_size;
// Binomial distribution for cluster size
for (int i = 1; i <= span; ++i) {
auto sample = noise_.noise(global_x, global_y, i) & 1;
cluster_size += sample; // Increase cluster size based on noise
}
DiscreteRandomNoiseStream rng(noise_, global_x, global_y, 48);
std::vector<TilePos> cluster_tiles;
std::unordered_set<TilePos> visited;
// Start with center if suitable
cluster_tiles.push_back(center);
visited.insert(center);
// Grow cluster using random walk
std::queue<TilePos> candidates;
candidates.push(center);
while (!candidates.empty() && cluster_tiles.size() < cluster_size) {
TilePos current = candidates.front();
candidates.pop();
auto neighbors = tilemap.get_neighbors(current);
std::shuffle(neighbors.begin(), neighbors.end(), rng);
for (const auto neighbor : neighbors) {
// 50% chance to skip this neighbor
auto [neighbor_global_x, neighbor_global_y] = neighbor.to_global();
auto sample = noise_.noise(
neighbor_global_x, neighbor_global_y,
0x2b52aaed // random seed
);
if ((sample & 1) == 0) {
continue;
}
if (visited.count(neighbor) > 0) {
continue; // Already visited
}
if (!is_suitable_for_oil(tilemap, neighbor)) {
continue; // Not suitable for oil
}
// Add to cluster
cluster_tiles.push_back(neighbor);
visited.insert(neighbor);
// Stop if we reached the desired cluster size
if (cluster_tiles.size() >= cluster_size) {
break;
}
candidates.push(neighbor);
}
}
// Place oil on all cluster tiles
for (const auto &pos : cluster_tiles) {
Tile &tile = tilemap.get_tile(pos);
tile.surface = SurfaceTileType::Oil;
}
}
bool OilGenerationPass::is_suitable_for_oil(
const TileMap &tilemap, TilePos pos
) const {
const Tile &tile = tilemap.get_tile(pos);
// Oil can only be placed on land or sand, and surface must be empty
return (tile.base == BaseTileType::Land || tile.base == BaseTileType::Sand)
&& tile.surface == SurfaceTileType::Empty;
}
std::uint8_t OilGenerationPass::get_biome_oil_preference(
BiomeType biome
) const {
std::uint8_t base_preference = config_.oil_base_probe;
switch (biome) {
case BiomeType::Desert:
case BiomeType::Plains:
return base_preference; // Full preference for desert/plains
case BiomeType::Savanna:
case BiomeType::SnowyPlains:
return (base_preference * 204) / 255; // ~80% preference
case BiomeType::Forest:
return (base_preference * 128) / 255; // ~50% preference
case BiomeType::SnowyPeeks:
return (base_preference * 77) / 255; // ~30% preference
case BiomeType::FrozenOcean:
case BiomeType::Ocean:
case BiomeType::LukeOcean:
default:
return 0; // No oil in oceans
}
}
std::uint32_t OilGenerationPass::calculate_min_oil_distance() const {
// Base minimum distance on chunk size and oil density
// Lower density = higher minimum distance
std::uint32_t base_distance = Chunk::size * 4 / 5;
return base_distance * 255 / config_.oil_density;
}
void TerrainGenerator::oil_pass(TileMap &tilemap) {
auto rng = master_rng_;
master_rng_ = master_rng_.jump_96();
auto noise_rng = master_rng_;
master_rng_ = master_rng_.jump_96();
OilGenerationPass pass(config_, rng, noise_rng);
pass(tilemap);
}
} // namespace istd

View File

@ -1,4 +1,5 @@
#include "tilemap.h" #include "tilemap.h"
#include "chunk.h"
#include <stdexcept> #include <stdexcept>
namespace istd { namespace istd {
@ -31,6 +32,14 @@ const Chunk &TileMap::get_chunk(
return chunks_[chunk_x][chunk_y]; return chunks_[chunk_x][chunk_y];
} }
Chunk &TileMap::get_chunk_of(TilePos pos) {
return get_chunk(pos.chunk_x, pos.chunk_y);
}
const Chunk &TileMap::get_chunk_of(TilePos pos) const {
return get_chunk(pos.chunk_x, pos.chunk_y);
}
Tile &TileMap::get_tile(TilePos pos) { Tile &TileMap::get_tile(TilePos pos) {
if (pos.chunk_x >= size_ || pos.chunk_y >= size_) { if (pos.chunk_x >= size_ || pos.chunk_y >= size_) {
throw std::out_of_range("Chunk coordinates out of bounds"); throw std::out_of_range("Chunk coordinates out of bounds");