aboutsummaryrefslogtreecommitdiff
path: root/src/shared/world
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 21:57:46 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 21:57:46 +1100
commite4483eca01b48b943cd0461e24a74ae1a3139ed4 (patch)
treeed58c3c246e3af1af337697695d780aa31f6ad9a /src/shared/world
parent1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (diff)
Update to most recent version (old initial commit)
Diffstat (limited to 'src/shared/world')
-rw-r--r--src/shared/world/block.cc85
-rw-r--r--src/shared/world/block.hh54
-rw-r--r--src/shared/world/chunk.cc1019
-rw-r--r--src/shared/world/chunk.hh89
4 files changed, 1247 insertions, 0 deletions
diff --git a/src/shared/world/block.cc b/src/shared/world/block.cc
new file mode 100644
index 0000000..4d66462
--- /dev/null
+++ b/src/shared/world/block.cc
@@ -0,0 +1,85 @@
+#include "shared/world/block.hh"
+
+namespace shared {
+namespace world {
+
+enum block::visibility
+block::get_visibility(const enum block::type type) noexcept {
+ switch (type) {
+ case type::air:
+ return visibility::invisible;
+ case type::shrub:
+ case type::dead_shrub:
+ case type::leaves:
+ case type::snowy_leaves:
+ case type::snowy_shrub:
+ return visibility::partial;
+ case type::water:
+ return visibility::translucent;
+ default:
+ break;
+ }
+ return visibility::solid;
+}
+
+bool block::is_tangible(const enum block::type type) noexcept {
+ switch (type) {
+ case type::air:
+ case type::shrub:
+ case type::dead_shrub:
+ case type::snowy_shrub:
+ return false;
+ default:
+ break;
+ }
+ return true;
+}
+
+bool block::is_collidable(const enum block::type type) noexcept {
+ switch (type) {
+ case type::air:
+ case type::shrub:
+ case type::dead_shrub:
+ case type::snowy_shrub:
+ case type::water:
+ return false;
+ default:
+ break;
+ }
+ return true;
+}
+
+bool block::is_liquid(const enum block::type type) noexcept {
+ switch (type) {
+ case type::water:
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+bool block::is_replaceable(const enum block::type type) noexcept {
+ switch (type) {
+ case type::water:
+ case type::air:
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+bool block::is_removable(const enum block::type type) noexcept {
+ switch (type) {
+ case type::water:
+ case type::air:
+ return false;
+ default:
+ break;
+ }
+ return true;
+}
+
+} // namespace world
+} // namespace shared
diff --git a/src/shared/world/block.hh b/src/shared/world/block.hh
new file mode 100644
index 0000000..f8d4e11
--- /dev/null
+++ b/src/shared/world/block.hh
@@ -0,0 +1,54 @@
+#ifndef SHARED_WORLD_BLOCK_BLOCK_HH_
+#define SHARED_WORLD_BLOCK_BLOCK_HH_
+
+#include <cstdint>
+
+namespace shared {
+namespace world {
+
+class block {
+public:
+ enum class type : std::uint8_t {
+ air, // zero means air which means don't render
+ dirt, // atlas should start from here
+ grass,
+ sand,
+ sandstone,
+ stone,
+ cobblestone,
+ wood,
+ leaves,
+ water,
+ shrub,
+ snow,
+ cactus,
+ dead_shrub,
+ snowy_wood,
+ snowy_leaves,
+ snowy_shrub,
+ };
+ enum class visibility { solid, partial, translucent, invisible };
+
+public:
+ type type;
+
+public:
+ // We prefer a static switch statement over constructing an object for
+ // speed.
+ static enum visibility get_visibility(const enum type type) noexcept;
+ static bool is_tangible(const enum type type) noexcept;
+ static bool is_collidable(const enum type type) noexcept;
+ static bool is_liquid(const enum type type) noexcept;
+ static bool is_replaceable(const enum type type) noexcept;
+ static bool is_removable(const enum type type) noexcept;
+
+public:
+ block(const enum type t = type::air) noexcept : type(t) {}
+
+ operator enum block::type() const noexcept { return this->type; }
+};
+
+} // namespace world
+} // namespace shared
+
+#endif
diff --git a/src/shared/world/chunk.cc b/src/shared/world/chunk.cc
new file mode 100644
index 0000000..b7dec5b
--- /dev/null
+++ b/src/shared/world/chunk.cc
@@ -0,0 +1,1019 @@
+#include "shared/world/chunk.hh"
+
+namespace shared {
+namespace world {
+
+// Because C++ doesn't allow us to iterate over an enum,
+// (or get the size of an enum, or get the name of an enum, etc) biomes defined
+// in the chunk::biome enum class have to be readded here. Alternatively we
+// could use some compiler hack library.
+constexpr std::array<enum shared::world::chunk::biome, 7> biome_enums{
+ shared::world::chunk::biome::alpine, shared::world::chunk::biome::tundra,
+ shared::world::chunk::biome::forest, shared::world::chunk::biome::plains,
+ shared::world::chunk::biome::ocean, shared::world::chunk::biome::islands,
+ shared::world::chunk::biome::desert};
+
+static unsigned long get_3d_index(const glm::ivec3& v) noexcept {
+#ifndef NDEBUG
+ if (chunk::is_outside_chunk(v)) {
+ throw std::range_error("bad chunk block coordinate access");
+ }
+#endif
+ return static_cast<unsigned long>(
+ v.x + (chunk::WIDTH * (v.y + (chunk::HEIGHT * v.z))));
+}
+
+constexpr long
+get_biome_index(const enum shared::world::chunk::biome biome) noexcept {
+ return std::distance(std::begin(biome_enums),
+ std::ranges::find(biome_enums, biome));
+}
+
+block& chunk::get_block(const glm::ivec3& v) noexcept {
+ return (*this->blocks)[get_3d_index(v)];
+}
+
+const block& chunk::get_block(const glm::ivec3& v) const noexcept {
+ return (*this->blocks)[get_3d_index(v)];
+}
+
+const char* chunk::get_biome(const int x, const int z) const noexcept {
+ const auto biome = (*this->biomes)[(unsigned long)x][(unsigned long)z];
+ switch (biome) {
+ case chunk::biome::desert:
+ return "desert";
+ case chunk::biome::islands:
+ return "islands";
+ case chunk::biome::forest:
+ return "forest";
+ case chunk::biome::plains:
+ return "plains";
+ case chunk::biome::ocean:
+ return "ocean";
+ case chunk::biome::tundra:
+ return "tundra";
+ case chunk::biome::alpine:
+ return "alpine";
+ }
+}
+
+bool chunk::is_outside_chunk(const glm::ivec3& v) noexcept {
+ return (std::min(v.x, v.z) < 0) || (std::max(v.x, v.z) >= WIDTH) ||
+ (std::clamp(v.y, 0, HEIGHT - 1) != v.y);
+}
+
+shared::math::coords
+chunk::get_normalised_chunk(const shared::math::coords& coords, const int x,
+ const int z) noexcept {
+ return coords +
+ shared::math::coords{(x >= WIDTH) - (x < 0), (z >= WIDTH) - (z < 0)};
+}
+
+std::pair<unsigned short, unsigned short>
+chunk::get_normalised_coords(const int x, const int z) noexcept {
+ return {x + WIDTH * ((x < 0) - (x >= WIDTH)),
+ z + WIDTH * ((z < 0) - (z >= WIDTH))};
+}
+
+glm::ivec3 chunk::get_normalised_coords(const glm::ivec3& c) noexcept {
+ const auto [x, z] = get_normalised_coords(c.x, c.z);
+ return {x, c.y, z};
+}
+
+// Returns a deterministic random number based on the args.
+static unsigned long make_prandom(const uint64_t seed,
+ const shared::math::coords& coords) noexcept {
+ const auto ulx = static_cast<unsigned long>(coords.x);
+ const auto ulz = static_cast<unsigned long>(coords.z);
+ return std::ranlux48{std::ranlux48{std::ranlux48{seed}() + ulx}() + ulz}();
+}
+
+// Returns a pseduorandom gradient vector.
+static glm::vec2 make_gvect(const std::uint64_t& seed,
+ const shared::math::coords& coords) noexcept {
+ const unsigned long pseudo = make_prandom(seed, coords);
+ // Return a vector based on the first four bits of this random number.
+ // This vector can point in dirs (x with a + on it) with range [-1, 1].
+ const float v1 = ((pseudo & 0b00001111) / 7.5f) - 1.0f;
+ const float v2 = (((pseudo & 0b11110000) >> 4) / 7.5f) - 1.0f;
+ return {v1, v2};
+}
+
+// Returns a distance vector between a vector and a grid position in a chunk.
+static glm::vec2 make_dvect(const glm::vec2 v, const unsigned int x,
+ const unsigned int z, const int width) noexcept {
+ const auto div = static_cast<float>(width - 1);
+ const float v1 = v.x - (static_cast<float>(x) / div);
+ const float v2 = v.y - (static_cast<float>(z) / div);
+ return {v1, v2};
+}
+
+static float fade(const float v) noexcept {
+ return v * v * v * (v * (v * 6 - 15) + 10);
+}
+
+static std::int32_t reflect_outer(const std::int32_t& n,
+ const int WIDTH = chunk::WIDTH) noexcept {
+ return n < 0 ? ((n - (WIDTH - 1)) / WIDTH) : n / WIDTH;
+}
+
+static std::int32_t reflect_inner(const std::int32_t& n,
+ const int WIDTH = chunk::WIDTH) noexcept {
+ return ((n % WIDTH) + WIDTH) % WIDTH;
+}
+
+// Moves perlin noise values from the range of [-1.0f, 1.0f] to [0.0f, 1.0f].
+// It's more useful to have a perlin layer add nothing at its absolute lowest,
+// then it is for a perlin layer to potentially remove 1.0f at its lowest.
+static float normalise_perlin(const float perlin) noexcept {
+ return 0.5f * (perlin + 1.0f);
+}
+
+using chunk_array = std::array<std::array<float, chunk::WIDTH>, chunk::WIDTH>;
+using chunk_array_map =
+ std::unordered_map<shared::math::coords, chunk_array,
+ decltype(&chunk::hash), decltype(&chunk::equal)>;
+
+// 2D Perlin noise, in which we:
+// 1. Define corners of a square as vectors, in this case using 0 or 1.
+// 2. Assign pseudorandom normalised gradient vectors to each corner vector.
+// 3. Create and iterate through a 2d array, where we:
+// 3.1: Generate distance vectors from each corner vector to the cell.
+// 3.2: Dot our gradient vectors and distance vectors respectively.
+// 3.3: Lerp our bottom and top dot product values by a fade function and x.
+// 3.4: Lerp (3.3) via fade function and z. This is the cell result.
+// There is an additional step where we make use of "scale" (any s* var). This
+// involves moving the requested chunk into a potentially different chunk, and
+// accessing a greater level of detail. It's just zoom and enhance.
+static chunk_array make_2d_perlin_array(const std::uint64_t& seed,
+ const shared::math::coords& pos,
+ const int scale) noexcept {
+ constexpr glm::vec2 tr = {1.0f, 1.0f}; // (1)
+ constexpr glm::vec2 tl = {0.0f, 1.0f};
+ constexpr glm::vec2 bl = {0.0f, 0.0f};
+ constexpr glm::vec2 br = {1.0f, 0.0f};
+
+ const int scx = reflect_outer(pos.x, scale);
+ const int scz = reflect_outer(pos.z, scale);
+ const int swidth = chunk::WIDTH * scale;
+
+ // clang-format off
+ const glm::vec2 tr_g = glm::normalize(make_gvect(seed, shared::math::coords{scx, scz})); // (2)
+ const glm::vec2 tl_g = glm::normalize(make_gvect(seed, shared::math::coords{scx - 1, scz}));
+ const glm::vec2 bl_g = glm::normalize(make_gvect(seed, shared::math::coords{scx - 1, scz - 1}));
+ const glm::vec2 br_g = glm::normalize(make_gvect(seed, shared::math::coords{scx, scz - 1}));
+ // clang-format on
+
+ chunk_array perlin; // (3)
+
+ const int x_offset = reflect_inner(pos.x, scale) * chunk::WIDTH;
+ const int z_offset = reflect_inner(pos.z, scale) * chunk::WIDTH;
+ for (auto x = 0u; x < chunk::WIDTH; ++x) {
+ const unsigned sx = x + static_cast<unsigned>(x_offset);
+
+ for (auto z = 0u; z < chunk::WIDTH; ++z) {
+ const unsigned sz = z + static_cast<unsigned>(z_offset);
+
+ const glm::vec2 tr_d = make_dvect(tr, sx, sz, swidth); // (3.1)
+ const glm::vec2 tl_d = make_dvect(tl, sx, sz, swidth);
+ const glm::vec2 bl_d = make_dvect(bl, sx, sz, swidth);
+ const glm::vec2 br_d = make_dvect(br, sx, sz, swidth);
+
+ const float tr_dp = glm::dot(tr_g, tr_d); // (3.2)
+ const float tl_dp = glm::dot(tl_g, tl_d);
+ const float bl_dp = glm::dot(bl_g, bl_d);
+ const float br_dp = glm::dot(br_g, br_d);
+
+ const float fswidth = static_cast<float>(swidth - 1);
+ const float fracx = (static_cast<float>(sx) + 0.5f) / fswidth;
+ const float fracz = (static_cast<float>(sz) + 0.5f) / fswidth;
+ const float tl_tr = std::lerp(tl_dp, tr_dp, fade(fracx)); // (3.3)
+ const float bl_br = std::lerp(bl_dp, br_dp, fade(fracx));
+
+ const float result = std::lerp(tl_tr, bl_br, fade(1.0f - fracz));
+
+ perlin[x][z] = normalise_perlin(result); // (3.4)
+ }
+ }
+
+ return perlin;
+}
+
+static auto array_map_access(const auto& array_map,
+ const shared::math::coords& coords) noexcept {
+ return array_map.find(coords)->second;
+}
+
+static auto array_map_access(const auto& array_map,
+ const shared::math::coords& coords, const int x,
+ const int z) noexcept {
+ const shared::math::coords normalised_coords =
+ chunk::get_normalised_chunk(coords, x, z);
+ const auto [nx, nz] = chunk::get_normalised_coords(x, z);
+ return array_map_access(array_map, normalised_coords)[nx][nz];
+}
+
+// We take a std::function that generates a chunk array to fill an unordered
+// map with a 3x3 chunk_array contents, where those contents refer to the
+// values in the surrounding chunks.
+template <typename T>
+static auto make_array_map(const std::uint64_t& seed,
+ const shared::math::coords& coords,
+ const T& make_chunk_array_func) noexcept {
+
+ std::unordered_map<shared::math::coords,
+ decltype(make_chunk_array_func(seed, coords)),
+ decltype(&chunk::hash), decltype(&chunk::equal)>
+ array_map{9, chunk::hash, chunk::equal};
+ for (int x = -1; x <= 1; ++x) {
+ for (int z = -1; z <= 1; ++z) {
+ const shared::math::coords pos{coords + shared::math::coords{x, z}};
+ array_map.emplace(pos, make_chunk_array_func(seed, pos));
+ }
+ }
+
+ return array_map;
+}
+
+// START VORONOI NOISE GENERATION FUNCTIONS
+// biome_*'s are used in the generation of voronoi noise which becomes the basis
+// of our biome definitions.
+using biome_array = std::array<
+ std::array<std::array<float, std::size(biome_enums)>, chunk::WIDTH>,
+ chunk::WIDTH>;
+using biome_array_map =
+ std::unordered_map<shared::math::coords, biome_array,
+ decltype(&chunk::hash), decltype(&chunk::equal)>;
+// Decreasing NUM_POINTS to the smallest meaningful value of 1 while
+// simultaneously decreasing our scale results in FAR lower computational
+// cost while achieving approximately the same result. This is because we
+// only have to generate 9 random numbers, as opposed to n * 9, which is a
+// performance KILLER. There is no reason to move this > 1.
+constexpr int NUM_BIOME_POINTS = 1;
+struct biome_point {
+ glm::vec2 pos;
+ enum chunk::biome biome;
+};
+
+using biome_point_array = std::array<struct biome_point, NUM_BIOME_POINTS>;
+static biome_point_array
+make_biome_point_array(const std::uint64_t& seed,
+ const shared::math::coords& coords) noexcept {
+
+ constexpr int BITS_PER_FLOAT = 48 / 2; // ranlux 48 / 2
+ constexpr int MASK = ((2 << BITS_PER_FLOAT) - 1);
+ static_assert(BITS_PER_FLOAT * 2 * NUM_BIOME_POINTS <= 48);
+
+ const unsigned long prand = make_prandom(seed, coords);
+ std::uniform_int_distribution<> uniform{0, std::size(biome_enums) - 1};
+ std::ranlux48 generator{prand};
+
+ biome_point_array points;
+ std::ranges::generate(points, [&, n = 0]() mutable -> biome_point {
+ const int x = (prand >> ((n + 1) * BITS_PER_FLOAT)) & MASK;
+ const int y = (prand >> ((n)*BITS_PER_FLOAT)) & MASK;
+ const glm::vec2 pos = glm::vec2{x, y} / static_cast<float>(MASK);
+ const auto biome = static_cast<chunk::biome>(uniform(generator));
+ ++n;
+ return {pos, biome};
+ });
+ return points;
+}
+
+// result of calling make_array_map with make_biome_point_array
+using biome_point_map = decltype(make_array_map(0u, shared::math::coords{0, 0},
+ make_biome_point_array));
+
+struct biome_point_info {
+ struct biome_point point;
+ glm::vec2 pos;
+ shared::math::coords coords;
+ float distance;
+};
+using biome_point_info_vector = std::vector<biome_point_info>;
+
+static biome_point_info_vector
+make_biome_point_info_vector(const biome_point_map& point_map,
+ const glm::vec2& pos,
+ const shared::math::coords& coords) noexcept {
+
+ biome_point_info_vector point_infos{};
+ for (int x = -1; x <= 1; ++x) {
+ for (int z = -1; z <= 1; ++z) {
+ const glm::vec2 offset = {x, z};
+ const biome_point_array& point_array = array_map_access(
+ point_map, coords + shared::math::coords{x, z});
+
+ for (const auto& point : point_array) {
+
+ const float distance = glm::distance(point.pos + offset, pos);
+ if (distance > 1.0f) {
+ continue;
+ }
+
+ point_infos.push_back(biome_point_info{
+ .point = point,
+ .pos = point.pos,
+ .coords = shared::math::coords{x, z} + coords,
+ .distance = distance});
+ }
+ }
+ }
+
+ return point_infos;
+}
+
+using biome_point_info_vector_array =
+ std::array<std::array<biome_point_info_vector, chunk::WIDTH>, chunk::WIDTH>;
+
+static biome_array make_2d_biome_array(const std::uint64_t& seed,
+ const shared::math::coords& coords,
+ const int scale) noexcept {
+
+ const shared::math::coords scaled_coords{reflect_outer(coords.x, scale),
+ reflect_outer(coords.z, scale)};
+
+ const auto point_2d_map =
+ make_array_map(seed, scaled_coords, make_biome_point_array);
+
+ const int x_offset = reflect_inner(coords.x, scale) * chunk::WIDTH;
+ const int z_offset = reflect_inner(coords.z, scale) * chunk::WIDTH;
+ const int scaled_width = chunk::WIDTH * scale;
+
+ // We generate a bit of perlin noise here so we can add some (jitter?)
+ // along our distances - the end result is less of an obvious line and more
+ // variance along our chunk borders.
+ constexpr int BIOME_JITTER_SCALE = 3;
+ const chunk_array jitter =
+ make_2d_perlin_array(seed, coords, BIOME_JITTER_SCALE);
+
+ // For our 2d array (ultimately columns of blocks in the world), we get the
+ // point that we're closest to in our world's voronoi noise points. For
+ // the points that are relevant (relatively close), we get the biome the
+ // point represents and add it to the column's array of biome influences.
+ // We ensure that these values add up to 1.0f. In the end, we have a struct
+ // that describes how much each biome affects each column as a %.
+ biome_array array = {};
+ for (auto x = 0u; x < chunk::WIDTH; ++x) {
+ const unsigned sx = x + static_cast<unsigned>(x_offset);
+
+ for (auto z = 0u; z < chunk::WIDTH; ++z) {
+ const unsigned sz = z + static_cast<unsigned>(z_offset);
+
+ // clang-format off
+ const glm::vec2 inner_pos =
+ (glm::vec2{static_cast<float>(sx) + 0.5f,
+ static_cast<float>(sz) + 0.5f}
+ / static_cast<float>(scaled_width)) +
+ (glm::vec2{std::sin(jitter[x][z]), std::cos(jitter[x][z])} * 0.1f);
+ // clang-format on
+
+ const auto point_infos = make_biome_point_info_vector(
+ point_2d_map, inner_pos, scaled_coords);
+
+ float total_dominance = 0.0f;
+ for (const auto& point_info : point_infos) {
+ const auto index = get_biome_index(point_info.point.biome);
+
+ const float dominance = std::clamp(
+ -1.0f * std::pow(0.5f * point_info.distance - 1.0f, 21.0f),
+ 0.0f, 1.0f);
+
+ auto& loc = array[x][z][static_cast<unsigned long>(index)];
+ if (loc > dominance) {
+ continue;
+ }
+ const float diff = dominance - loc;
+ loc += diff;
+ total_dominance += diff;
+ }
+
+ for (float& dominance : array[x][z]) {
+ dominance *= (1.0f / total_dominance);
+ }
+ }
+ }
+ return array;
+}
+// END VORONOI NOISE GENERATION FUNCTIONS
+
+// These are constexpr for our static assert.
+static constexpr float
+get_biome_offset(const enum chunk::biome biome) noexcept {
+ switch (biome) {
+ case chunk::biome::ocean:
+ case chunk::biome::islands:
+ return 0.0f;
+ default:
+ break;
+ }
+ return 10.0f;
+}
+
+static constexpr float
+get_biome_variation17(const enum chunk::biome biome) noexcept {
+ switch (biome) {
+ case chunk::biome::alpine:
+ return 80.0f;
+ case chunk::biome::tundra:
+ return 10.0f;
+ case chunk::biome::forest:
+ return 30.0f;
+ case chunk::biome::ocean:
+ return 0.0f;
+ case chunk::biome::islands:
+ return 5.0f;
+ default:
+ break;
+ }
+ return 15.0f;
+}
+
+static constexpr float
+get_biome_variation11(const enum chunk::biome biome) noexcept {
+ switch (biome) {
+ break;
+ case chunk::biome::alpine:
+ return 40.0f;
+ case chunk::biome::tundra:
+ return 30.0f;
+ default:
+ break;
+ }
+ return 20.0f;
+}
+
+static constexpr float
+get_biome_variation7(const enum chunk::biome biome) noexcept {
+ switch (biome) {
+ case chunk::biome::alpine:
+ return 20.0f;
+ case chunk::biome::islands:
+ return 30.0f;
+ case chunk::biome::desert:
+ case chunk::biome::plains:
+ return 15.0f;
+ default:
+ break;
+ }
+ return 10.0f;
+}
+
+static constexpr float
+get_biome_variation3(const enum chunk::biome biome) noexcept {
+ switch (biome) {
+ default:
+ break;
+ }
+ return 7.5f;
+}
+
+constexpr float BASE_HEIGHT = 40.0f;
+// Ensure any perlin values of our biome generation does not result in a y value
+// that is outside our max height.
+static_assert(std::ranges::all_of(biome_enums, [](const auto& biome) {
+ const float max_height =
+ BASE_HEIGHT + get_biome_offset(biome) + get_biome_variation3(biome) +
+ get_biome_variation7(biome) + get_biome_variation11(biome) +
+ get_biome_variation17(biome);
+ return max_height < static_cast<float>(chunk::HEIGHT);
+}));
+
+// Line that crosses at 0.5f, 0.5f with a variable gradient, should be clamped.
+static float linear_gradient(const float x, const float m) noexcept {
+ return m * (x - (0.5f - (0.5f / m)));
+}
+
+// The functions for ...these functions... should be domain and range [0, 1].
+static float process_variation(float variation,
+ const enum chunk::biome biome) noexcept {
+ switch (biome) {
+ case chunk::biome::alpine:
+ variation = linear_gradient(variation, 2.2f);
+ break;
+ default:
+ variation = linear_gradient(variation, 1.5f);
+ break;
+ }
+ return std::clamp(variation, 0.0f, 1.0f);
+}
+
+chunk_array make_topography(const std::uint64_t& seed,
+ const shared::math::coords& coords,
+ const biome_array_map& biome_map) noexcept {
+
+ chunk_array topography = {};
+
+ const biome_array& biomes = biome_map.find(coords)->second;
+ const chunk_array perlin17 = make_2d_perlin_array(seed, coords, 17);
+ const chunk_array perlin11 = make_2d_perlin_array(seed, coords, 11);
+ const chunk_array perlin7 = make_2d_perlin_array(seed, coords, 7);
+ const chunk_array perlin3 = make_2d_perlin_array(seed, coords, 3);
+
+ for (auto x = 0ul; x < chunk::WIDTH; ++x) {
+ for (auto z = 0ul; z < chunk::WIDTH; ++z) {
+
+ // Initial topography of 40.0f.
+ topography[x][z] = BASE_HEIGHT;
+
+ const auto biome_dominance = biomes[x][z];
+ for (auto i = 0u; i < std::size(biome_dominance); ++i) {
+ const enum chunk::biome biome = biome_enums[i];
+
+ const float dominance = biome_dominance[i];
+ const float v3 = process_variation(perlin3[x][z], biome) *
+ get_biome_variation3(biome);
+ const float v7 = process_variation(perlin7[x][z], biome) *
+ get_biome_variation7(biome);
+ const float v11 = process_variation(perlin11[x][z], biome) *
+ get_biome_variation11(biome);
+ const float v17 = process_variation(perlin17[x][z], biome) *
+ get_biome_variation17(biome);
+
+ topography[x][z] +=
+ (get_biome_offset(biome) + v3 + v7 + v11 + v17) * dominance;
+ }
+ }
+ }
+
+ return topography;
+}
+
+static chunk_array
+make_probabilities(const std::uint64_t& seed,
+ const shared::math::coords& coords) noexcept {
+
+ chunk_array chunk_array;
+
+ std::uniform_real_distribution<float> uniform{
+ 0.0f, std::nextafter(1.0f, std::numeric_limits<float>::max())};
+ std::ranlux48 generator(make_prandom(seed, coords));
+ for (auto x = 0ul; x < chunk::WIDTH; ++x) {
+ for (auto z = 0ul; z < chunk::WIDTH; ++z) {
+ chunk_array[x][z] = uniform(generator);
+ }
+ }
+
+ return chunk_array;
+}
+
+static biome_array make_biomes(const std::uint64_t& seed,
+ const shared::math::coords& coords) noexcept {
+
+ constexpr int BIOME_SCALE = 99;
+ return make_2d_biome_array(seed, coords, BIOME_SCALE);
+}
+
+constexpr int SAND_HEIGHT = 65;
+constexpr int WATER_HEIGHT = 63;
+
+constexpr int SAND_DEPTH = 5;
+constexpr int SANDSTONE_DEPTH = 6;
+constexpr int GRASS_DEPTH = 1;
+constexpr int DIRT_DEPTH = 7;
+
+// Rulesets common to all chunks go here. Water inhabits all blocks below 63
+// and above the topography value. In addition, stone fills all blocks after
+// the initial biome blocks.
+static void generate_post_terrain_column(chunk::block_array_t& blocks,
+ const glm::ivec3& pos,
+ const int lowest) noexcept {
+ for (int y = lowest; y > WATER_HEIGHT; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = block::type::sand;
+ }
+ for (int y = WATER_HEIGHT; y > pos.y; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = block::type::water;
+ }
+ for (int y = lowest; y >= 0; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = block::type::stone;
+ }
+}
+
+static int generate_sandy_terrain_column(chunk::block_array_t& blocks,
+ const glm::ivec3& pos) noexcept {
+ int y = pos.y;
+ for (const int lowest = y - SAND_DEPTH; y > lowest; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = block::type::sand;
+ }
+ for (const int lowest = y - SANDSTONE_DEPTH; y > lowest; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = block::type::sandstone;
+ }
+ return y;
+}
+
+static int generate_grassy_terrain_column(chunk::block_array_t& blocks,
+ const glm::ivec3& pos,
+ const enum block::type top) noexcept {
+ int y = pos.y;
+ if (y <= SAND_HEIGHT) {
+ for (const int lowest = y - SAND_DEPTH; y > lowest; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = block::type::sand;
+ }
+ } else {
+ for (const int lowest = y - GRASS_DEPTH; y > lowest; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = top;
+ }
+ for (const int lowest = y - DIRT_DEPTH; y > lowest; --y) {
+ blocks[get_3d_index({pos.x, y, pos.z})] = block::type::dirt;
+ }
+ }
+ return y;
+}
+
+static void generate_terrain_column(chunk::block_array_t& blocks,
+ const glm::ivec3& pos,
+ const enum chunk::biome biome) noexcept {
+ const int lowest = [&]() {
+ switch (biome) {
+ case chunk::biome::desert:
+ return generate_sandy_terrain_column(blocks, pos);
+
+ case chunk::biome::islands:
+ case chunk::biome::forest:
+ case chunk::biome::plains:
+ case chunk::biome::ocean:
+ return generate_grassy_terrain_column(blocks, pos,
+ block::type::grass);
+
+ case chunk::biome::tundra:
+ case chunk::biome::alpine:
+ return generate_grassy_terrain_column(blocks, pos,
+ block::type::snow);
+ }
+ }();
+
+ generate_post_terrain_column(blocks, pos, lowest);
+}
+
+static enum chunk::biome get_dominant_biome(const auto& array) noexcept {
+ const auto max_it =
+ std::max_element(std::begin(array), std::end(array),
+ [](const auto& a, const auto& b) { return a < b; });
+ return biome_enums[static_cast<unsigned long>(
+ std::distance(std::begin(array), max_it))];
+}
+
+static void generate_terrain(chunk::block_array_t& blocks,
+ const shared::math::coords& coords,
+ const chunk_array_map& topography_map,
+ const biome_array_map& biome_map) noexcept {
+
+ const biome_array& biomes = biome_map.find(coords)->second;
+ const chunk_array& topography = topography_map.find(coords)->second;
+
+ // We fill in our chunk column by column, where the column refers to a
+ // function which maps to a biome.
+ for (unsigned long x = 0ul; x < chunk::WIDTH; ++x) {
+ for (unsigned long z = 0ul; z < chunk::WIDTH; ++z) {
+ generate_terrain_column(blocks, glm::vec3{x, topography[x][z], z},
+ get_dominant_biome(biomes[x][z]));
+ }
+ }
+}
+
+// Objects have a max WIDTH, but no max WIDTH.
+constexpr int MAX_OBJECT_WIDTH = 5;
+constexpr int OBJECT_HALFWIDTH = MAX_OBJECT_WIDTH / 2;
+// A layer is an x, z slice of an object.
+using object_layer =
+ std::array<std::array<block, MAX_OBJECT_WIDTH>, MAX_OBJECT_WIDTH>;
+using object = std::vector<object_layer>;
+struct biome_object {
+ const object* const blocks;
+ const float probability;
+ const int altitude; // lowest alt we can gen at
+};
+using biome_objects = std::vector<biome_object>;
+
+static object make_tree_object() noexcept {
+ object tree;
+ object_layer layer = {};
+
+ layer[OBJECT_HALFWIDTH][OBJECT_HALFWIDTH] = block::type::wood;
+ for (int i = 0; i < 3; ++i) {
+ tree.push_back(layer);
+ }
+
+ for (unsigned x = 0; x < std::size(layer); ++x) {
+ for (unsigned z = 0; z < std::size(layer); ++z) {
+ if (x == OBJECT_HALFWIDTH && z == OBJECT_HALFWIDTH) {
+ continue;
+ }
+ layer[x][z] = block::type::leaves;
+ }
+ }
+ for (int i = 0; i < 2; ++i) {
+ tree.push_back(layer);
+ }
+ for (unsigned x = 0; x < std::size(layer); ++x) {
+ for (unsigned z = 0; z < std::size(layer); ++z) {
+ if (!(x == 0 || x == 4 || z == 0 || z == 4)) {
+ continue;
+ }
+ layer[x][z] = block::type::air;
+ }
+ }
+ tree.push_back(layer);
+ for (unsigned x = 0; x < std::size(layer); ++x) {
+ for (unsigned z = 0; z < std::size(layer); ++z) {
+ if (x < 1 || x > 3 || z < 1 || z > 3 ||
+ (std::abs(int(x) - 2) + std::abs(int(z) - 2) == 2)) {
+ layer[x][z] = block::type::air;
+ continue;
+ }
+ layer[x][z] = block::type::leaves;
+ }
+ }
+ tree.push_back(layer);
+ return tree;
+}
+
+static object make_alpine_tree() noexcept {
+ object tree;
+ object_layer layer = {};
+
+ layer[OBJECT_HALFWIDTH][OBJECT_HALFWIDTH] = block::type::snowy_wood;
+ constexpr int HEIGHT = 10;
+ for (int y = 0; y <= HEIGHT; ++y) {
+
+ constexpr float BASE_RAD = static_cast<float>(MAX_OBJECT_WIDTH) / 2.0f;
+
+ const float radius =
+ BASE_RAD * std::exp(-1.0f * (static_cast<float>(y) /
+ static_cast<float>(HEIGHT)));
+
+ for (unsigned x = 0; x < MAX_OBJECT_WIDTH; ++x) {
+ for (unsigned z = 0; z < MAX_OBJECT_WIDTH; ++z) {
+ if (x == OBJECT_HALFWIDTH && z == OBJECT_HALFWIDTH) {
+ if (y == HEIGHT) {
+ layer[x][z] = block::type::snowy_leaves;
+ }
+
+ continue; // leave as wood
+ }
+
+ if ((y + 1) % 2) {
+ layer[x][z] = block::type::air;
+ continue;
+ }
+
+ const float lhs = std::pow((float)x - 2.0f, 2.0f) +
+ std::pow((float)z - 2.0f, 2.0f);
+ const float rhs = std::pow(radius, 2.0f);
+
+ layer[x][z] =
+ lhs < rhs ? block::type::snowy_leaves : block::type::air;
+ }
+ }
+
+ tree.push_back(layer);
+ }
+
+ return tree;
+}
+
+static object make_column_object(const enum block::type type,
+ const int WIDTH) noexcept {
+ object obj;
+ object_layer layer = {};
+
+ layer[OBJECT_HALFWIDTH][OBJECT_HALFWIDTH] = type;
+ for (int i = 0; i < WIDTH; ++i) {
+ obj.push_back(layer);
+ }
+ return obj;
+}
+
+static const biome_objects&
+get_biome_objects(const chunk::biome biome) noexcept {
+ using btype = enum block::type;
+ static const object tree = make_tree_object();
+ static const object alpine_tree = make_alpine_tree();
+ static const object cactus1 = make_column_object(btype::cactus, 1);
+ static const object cactus2 = make_column_object(btype::cactus, 2);
+ static const object cactus3 = make_column_object(btype::cactus, 3);
+ static const object grass = make_column_object(btype::shrub, 1);
+ static const object dead_shrub = make_column_object(btype::dead_shrub, 1);
+ static const object snowy_grass = make_column_object(btype::snowy_shrub, 1);
+
+ switch (biome) {
+ case chunk::biome::islands:
+ static const biome_objects island_objects{
+ biome_object{&tree, 0.0005f, SAND_HEIGHT},
+ biome_object{&grass, 0.025f, SAND_HEIGHT}};
+ return island_objects;
+
+ case chunk::biome::plains:
+ static const biome_objects plains_objects{
+ biome_object{&tree, 0.0005f, SAND_HEIGHT},
+ biome_object{&grass, 0.1f, SAND_HEIGHT}};
+ return plains_objects;
+
+ case chunk::biome::forest:
+ static const biome_objects forest_objects{
+ biome_object{&tree, 0.01f, SAND_HEIGHT},
+ biome_object{&grass, 0.1f, SAND_HEIGHT}};
+ return forest_objects;
+
+ case chunk::biome::desert:
+ static const biome_objects desert_objects{
+ biome_object{&cactus1, 0.0002f, WATER_HEIGHT},
+ biome_object{&cactus2, 0.0005f, WATER_HEIGHT},
+ biome_object{&cactus3, 0.0009f, WATER_HEIGHT},
+ biome_object{&dead_shrub, 0.0012f, WATER_HEIGHT}};
+ return desert_objects;
+
+ case chunk::biome::alpine:
+ static const biome_objects alpine_objects{
+ biome_object{&alpine_tree, 0.0008f, SAND_HEIGHT},
+ biome_object{&snowy_grass, 0.0025f, SAND_HEIGHT}};
+ return alpine_objects;
+
+ case chunk::biome::tundra:
+ static const biome_objects tundra_objects{
+ biome_object{&alpine_tree, 0.0008f, SAND_HEIGHT},
+ biome_object{&snowy_grass, 0.004f, SAND_HEIGHT}};
+ return tundra_objects;
+
+ case chunk::biome::ocean:
+ static const biome_objects ocean_objects{
+ biome_object{&tree, 0.001f, SAND_HEIGHT},
+ biome_object{&grass, 0.1f, SAND_HEIGHT}};
+ return ocean_objects;
+ }
+}
+
+static std::optional<biome_object>
+maybe_get_object(const float probability, const chunk::biome biome) noexcept {
+ const auto& biome_objects = get_biome_objects(biome);
+
+ const auto find_it =
+ std::ranges::find_if(biome_objects, [&](const auto& object) {
+ return object.probability >= probability;
+ });
+
+ if (find_it != std::end(biome_objects)) {
+ return *find_it;
+ }
+ return std::nullopt;
+}
+
+static int get_block_priority(const enum block::type& b) noexcept {
+ switch (b) {
+ case block::type::air:
+ return 0;
+ case block::type::leaves:
+ case block::type::snowy_leaves:
+ return 1;
+ default:
+ break;
+ }
+ return 2;
+}
+
+static void generate_object(const object& object, chunk::block_array_t& blocks,
+ const glm::ivec3& pos) noexcept {
+ for (unsigned y = 0; y < std::size(object); ++y) {
+
+ const auto& layer = object[y];
+ for (unsigned x = 0; x < std::size(layer); ++x) {
+ for (unsigned z = 0; z < std::size(layer); ++z) {
+
+ const block block = layer[x][z];
+ if (block == block::type::air) {
+ continue;
+ }
+
+ const glm::ivec3 block_pos =
+ pos + glm::ivec3{x - MAX_OBJECT_WIDTH / 2, y + 1,
+ z - MAX_OBJECT_WIDTH / 2};
+ if (chunk::is_outside_chunk(block_pos)) {
+ continue;
+ }
+
+ const auto index = get_3d_index(block_pos);
+
+ const class block old_block = blocks[index];
+ if (get_block_priority(block) < get_block_priority(old_block)) {
+ continue;
+ }
+
+ blocks[index] = block;
+ }
+ }
+ }
+}
+
+static void generate_objects(chunk::block_array_t& blocks,
+ const shared::math::coords& coords,
+ const chunk_array_map& topography_map,
+ const biome_array_map& biome_map,
+ const chunk_array_map& probability_map) noexcept {
+
+ // This is really expensive as it is x^2
+ // We don't want to create any structures that are very wide as a result.
+ // (at least, structures which generate via this method).
+ const int MAX_READ = MAX_OBJECT_WIDTH / 2;
+ for (int x = -MAX_READ; x < MAX_READ + chunk::WIDTH; ++x) {
+ for (int z = -MAX_READ; z < MAX_READ + chunk::WIDTH; ++z) {
+ const float probability =
+ array_map_access(probability_map, coords, x, z);
+ const chunk::biome biome =
+ get_dominant_biome(array_map_access(biome_map, coords, x, z));
+
+ const auto& object = maybe_get_object(probability, biome);
+ if (!object.has_value()) {
+ continue;
+ }
+
+ const glm::ivec3 pos{
+ x, array_map_access(topography_map, coords, x, z), z};
+
+ if (pos.y <= object->altitude) {
+ continue;
+ }
+
+ generate_object(*object->blocks, blocks, pos);
+ }
+ }
+}
+
+static chunk::biomes_t make_biome_array(const biome_array& array) noexcept {
+ auto ret = std::make_unique<chunk::biome_array_t>();
+ for (unsigned long x = 0; x < chunk::WIDTH; ++x) {
+ for (unsigned long z = 0; z < chunk::WIDTH; ++z) {
+ (*ret)[x][z] = get_dominant_biome(array[x][z]);
+ }
+ }
+ return ret;
+}
+
+// Use perlin noise and extrapolation of eigen vectors along a mobius strip.
+chunk::chunk(const std::uint64_t& seed,
+ const shared::math::coords& coords) noexcept
+ : pos(coords) {
+ const biome_array_map biome_arrays =
+ make_array_map(seed, coords, make_biomes);
+ // Topography relies on block temperature so we bind it as a hidden
+ // third argument as to avoid unnecessary generation.
+ const chunk_array_map topography_arrays =
+ make_array_map(seed, coords,
+ std::bind(make_topography, std::placeholders::_1,
+ std::placeholders::_2, biome_arrays));
+
+ const chunk_array_map probability_map =
+ make_array_map(seed, coords, make_probabilities);
+
+ chunk::blocks_t ret = std::make_unique<chunk::block_array_t>();
+
+ generate_terrain(*ret, coords, topography_arrays, biome_arrays);
+ generate_objects(*ret, coords, topography_arrays, biome_arrays,
+ probability_map);
+ // generate caves
+ // generate better terrain generation
+
+ this->blocks = std::move(ret);
+ this->biomes = make_biome_array(biome_arrays.find(coords)->second);
+}
+
+chunk::chunk(const std::uint64_t& seed, const proto::chunk& proto) noexcept
+ : pos(shared::net::get_coords(proto.chunk_pos())) {
+
+ blocks_t ret = std::make_unique<chunk::block_array_t>();
+
+ for (int i = 0; i < proto.blocks_size(); ++i) {
+ const std::uint32_t packed_blocks = proto.blocks(i);
+ for (int j = 0; j < 4; ++j) {
+ (*ret)[static_cast<unsigned>(i * 4 + j)] =
+ static_cast<enum block::type>(
+ static_cast<std::uint8_t>(packed_blocks >> j * 8));
+ }
+ }
+
+ // Our protobuf constructor doesn't call any of the worldgen stuff, but
+ // needs to gen the biomes locally to populate this->biomes.
+ this->blocks = std::move(ret);
+ this->biomes = make_biome_array(make_biomes(seed, this->pos));
+}
+
+void chunk::pack(proto::chunk* const proto) const noexcept {
+ shared::net::set_coords(proto->mutable_chunk_pos(), this->pos);
+
+ // Since protobuf can store at minimum uint32, we mash four of our
+ // uint_8 chunk blocks into a single uint32.
+ static_assert(shared::world::chunk::VOLUME % 4 == 0);
+ for (unsigned i = 0u; i < shared::world::chunk::VOLUME / 4u; ++i) {
+ std::uint32_t packed_blocks = 0u;
+
+ for (unsigned j = 0; j < 4; ++j) {
+ const auto block =
+ static_cast<std::uint8_t>((*this->blocks)[i * 4 + j].type);
+ packed_blocks |= static_cast<unsigned>(block << j * 8);
+ }
+
+ proto->add_blocks(packed_blocks);
+ }
+}
+
+} // namespace world
+} // namespace shared
diff --git a/src/shared/world/chunk.hh b/src/shared/world/chunk.hh
new file mode 100644
index 0000000..8e724a3
--- /dev/null
+++ b/src/shared/world/chunk.hh
@@ -0,0 +1,89 @@
+#ifndef SHARED_WORLD_CHUNK_HH_
+#define SHARED_WORLD_CHUNK_HH_
+
+#include <algorithm>
+#include <array>
+#include <exception>
+#include <functional>
+#include <istream>
+#include <memory>
+#include <random>
+#include <type_traits>
+#include <vector>
+
+#include <boost/functional/hash.hpp>
+#include <glm/glm.hpp>
+
+#include "shared/math/math.hh"
+#include "shared/net/proto.hh"
+#include "shared/shared.hh"
+#include "shared/world/block.hh"
+
+namespace shared {
+namespace world {
+
+class chunk {
+public:
+ static constexpr int WIDTH = 16;
+ static constexpr int HEIGHT = 256;
+ static constexpr int VOLUME = WIDTH * WIDTH * HEIGHT;
+
+ // Stuff for unordered_map.
+ static std::size_t hash(const shared::math::coords& c) noexcept {
+ std::size_t seed = 0;
+ boost::hash_combine(seed, boost::hash_value(c.x));
+ boost::hash_combine(seed, boost::hash_value(c.z));
+ return seed;
+ }
+ static std::size_t equal(const shared::math::coords& a,
+ const shared::math::coords& b) noexcept {
+ return a == b;
+ }
+ enum class biome { desert, islands, ocean, plains, forest, tundra, alpine };
+
+ using block_array_t = std::array<block, VOLUME>;
+ using blocks_t = std::unique_ptr<block_array_t>;
+ using biome_array_t = std::array<std::array<enum biome, WIDTH>, WIDTH>;
+ using biomes_t = std::unique_ptr<biome_array_t>;
+
+protected:
+ shared::math::coords pos;
+ blocks_t blocks; // Use get_block for 3d index access.
+
+ // Simple 2d array of enums where each column maps to the dominant biome
+ // at that x, z location. Call get_biome_str for string.
+ biomes_t biomes;
+
+public:
+ static bool is_outside_chunk(const glm::ivec3& v) noexcept;
+
+ static shared::math::coords
+ get_normalised_chunk(const shared::math::coords& coords, const int x,
+ const int z) noexcept;
+
+ static std::pair<unsigned short, unsigned short>
+ get_normalised_coords(const int x, const int z) noexcept;
+
+ static glm::ivec3 get_normalised_coords(const glm::ivec3& c) noexcept;
+
+public:
+ chunk(const std::uint64_t& seed,
+ const shared::math::coords& coords) noexcept;
+ chunk(const std::uint64_t& seed, const proto::chunk& chunk) noexcept;
+ virtual ~chunk() noexcept {};
+
+protected:
+ void pack(proto::chunk* const proto) const noexcept;
+
+public:
+ block& get_block(const glm::ivec3& v) noexcept;
+ const block& get_block(const glm::ivec3& v) const noexcept;
+ const shared::math::coords& get_pos() const noexcept { return this->pos; }
+
+ const char* get_biome(const int x, const int z) const noexcept;
+};
+
+} // namespace world
+} // namespace shared
+
+#endif