aboutsummaryrefslogtreecommitdiff
path: root/src/client/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/client/world
parent1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (diff)
Update to most recent version (old initial commit)
Diffstat (limited to 'src/client/world')
-rw-r--r--src/client/world/block.cc117
-rw-r--r--src/client/world/block.hh53
-rw-r--r--src/client/world/chunk.cc255
-rw-r--r--src/client/world/chunk.hh82
-rw-r--r--src/client/world/world.datbin0 -> 20480 bytes
5 files changed, 507 insertions, 0 deletions
diff --git a/src/client/world/block.cc b/src/client/world/block.cc
new file mode 100644
index 0000000..df3ddcd
--- /dev/null
+++ b/src/client/world/block.cc
@@ -0,0 +1,117 @@
+#include "client/world/block.hh"
+
+namespace client {
+namespace world {
+
+struct glvert_args {
+ glm::vec3 translate;
+ float rotate_degrees;
+ glm::vec3 rotate_axis;
+ glm::vec3 texture_offset;
+};
+static block::glface_t make_glface(const glvert_args& args) noexcept {
+ static constexpr block::glface_t glverts = {
+ block::glvert{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}},
+ block::glvert{{0.5f, -0.5f, 0.0f}, {1.0f, 1.0f, 0.0f}},
+ block::glvert{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+ block::glvert{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+ block::glvert{{-0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 0.0f}},
+ block::glvert{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}}};
+
+ // We have to be careful here not to rotate/translate a zero vector.
+ constexpr auto zero = glm::vec3{};
+ const glm::mat4 mtranslate =
+ args.translate == zero
+ ? glm::mat4{1.0f}
+ : glm::translate(glm::mat4{1.0f}, args.translate);
+ const glm::mat4 mrotate =
+ args.rotate_axis == zero
+ ? glm::mat4{1.0f}
+ : glm::rotate(glm::mat4{1.0f}, glm::radians(args.rotate_degrees),
+ args.rotate_axis);
+
+ block::glface_t ret;
+ std::ranges::transform(glverts, std::begin(ret), [&](auto f) {
+ f.vertice =
+ glm::vec3(mtranslate * mrotate * glm::vec4{f.vertice, 1.0f});
+ f.texture += args.texture_offset;
+ return f;
+ });
+ return ret;
+}
+
+static const block::glfaces_t& get_shrub_faces() noexcept {
+ static block::glfaces_t faces{
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 45.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 135.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 225.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, 0.0f},
+ .rotate_degrees = 315.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}})};
+ return faces;
+}
+
+static const block::glfaces_t& get_cube_faces() noexcept {
+ static block::glfaces_t faces{
+ make_glface({.translate = {0.0f, 0.0f, 0.5f},
+ .rotate_degrees = 0.0f,
+ .rotate_axis = {0.0f, 0.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 0.0f}}),
+ make_glface({.translate = {0.5f, 0.0f, 0.0f},
+ .rotate_degrees = 90.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 1.0f}}),
+ make_glface({.translate = {0.0f, 0.0f, -0.5f},
+ .rotate_degrees = 180.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 2.0f}}),
+ make_glface({.translate = {-0.5f, 0.0f, 0.0f},
+ .rotate_degrees = 270.0f,
+ .rotate_axis = {0.0f, 1.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 3.0f}}),
+ make_glface({.translate = {0.0f, 0.5f, 0.0f},
+ .rotate_degrees = -90.0f,
+ .rotate_axis = {1.0f, 0.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 4.0f}}),
+ make_glface({.translate = {0.0f, -0.5f, 0.0f},
+ .rotate_degrees = 90.0f,
+ .rotate_axis = {1.0f, 0.0f, 0.0f},
+ .texture_offset = {0.0f, 0.0f, 5.0f}}),
+ };
+ return faces;
+}
+
+enum block::draw_type
+block::get_draw_type(const enum block::type& type) noexcept {
+ using t = enum shared::world::block::type;
+ switch (type) {
+ case t::dead_shrub:
+ case t::shrub:
+ case t::snowy_shrub:
+ return draw_type::custom;
+ default:
+ return draw_type::block;
+ }
+}
+
+const block::glfaces_t&
+block::get_glfaces(const enum block::type& type) noexcept {
+ const auto draw_type = get_draw_type(type);
+ if (draw_type == block::draw_type::custom) {
+ return get_shrub_faces();
+ }
+ return get_cube_faces();
+}
+
+} // namespace world
+} // namespace client
diff --git a/src/client/world/block.hh b/src/client/world/block.hh
new file mode 100644
index 0000000..9dfe303
--- /dev/null
+++ b/src/client/world/block.hh
@@ -0,0 +1,53 @@
+#ifndef CLIENT_WORLD_BLOCK_HH_
+#define CLIENT_WORLD_BLOCK_HH_
+
+#include <algorithm>
+#include <array>
+#include <vector>
+
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_access.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+
+#include "shared/world/block.hh"
+
+namespace client {
+namespace world {
+
+// Doesn't add any data, just information for rendering.
+class block : public shared::world::block {
+public:
+ struct glvert {
+ glm::vec3 vertice;
+ glm::vec3 texture;
+ };
+ using glface_t = std::array<glvert, 6>; // array of verts (a face)
+ using glfaces_t = std::vector<glface_t>; // vector of faces (a block)
+
+ // Render types refer to how the block should be culled when making the vbo.
+ enum class draw_type {
+ block, // face testing
+ custom, // no testing
+ };
+
+public:
+ static enum draw_type get_draw_type(const enum block::type& type) noexcept;
+ static const glfaces_t& get_glfaces(const enum block::type& type) noexcept;
+
+public:
+ template <typename... Args>
+ block(Args&&... args) noexcept : block(std::forward<Args>(args)...) {}
+
+public:
+ enum draw_type get_draw_type() const noexcept {
+ return get_draw_type(this->type);
+ }
+ const glfaces_t& get_glfaces() const noexcept {
+ return get_glfaces(this->type);
+ }
+};
+
+} // namespace world
+} // namespace client
+
+#endif
diff --git a/src/client/world/chunk.cc b/src/client/world/chunk.cc
new file mode 100644
index 0000000..99720cb
--- /dev/null
+++ b/src/client/world/chunk.cc
@@ -0,0 +1,255 @@
+#include "client/world/chunk.hh"
+
+namespace client {
+namespace world {
+
+void chunk::render(const float world_x, const float world_z,
+ const pass& pass) noexcept {
+ const auto make_matrix = [&]() -> glm::mat4 {
+ const auto& proj = client::render::camera::get_proj();
+ const auto& view = client::render::camera::get_view();
+ return glm::translate(proj * view, glm::vec3{world_x, 0, world_z});
+ };
+ static client::render::program program{"res/shaders/face.vs",
+ "res/shaders/face.fs"};
+ static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix");
+
+ const GLuint texture [[maybe_unused]] = client::render::get_texture_atlas();
+ glDisable(GL_BLEND);
+ glEnable(GL_DEPTH_TEST);
+ glUseProgram(program);
+ // Our choice of vao depends on which pass we're doing.
+ const auto [vao, elements] = [&pass, this]() -> std::pair<GLuint, GLuint> {
+ if (pass == pass::solid) {
+ return {this->glo->solid_vao, this->glo->solid_elements};
+ }
+ return {this->glo->water_vao, this->glo->water_elements};
+ }();
+ glBindVertexArray(vao);
+
+ glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix()));
+
+ glDrawArrays(GL_TRIANGLES, 0, elements);
+}
+
+const chunk* chunk::get_neighbour(const chunks_t& chunks,
+ shared::math::coords offset) const noexcept {
+ const auto find_it = chunks.find(this->pos + offset);
+ if (find_it == std::end(chunks) || !find_it->second.has_value()) {
+ return nullptr;
+ }
+ return &((*find_it).second.value());
+}
+
+bool chunk::maybe_regenerate_glo(const chunks_t& chunks) noexcept {
+ // We need all surrounding chunks to make our vbo, so early out with false
+ // if we can't do that yet.
+ const auto chunk_forward = this->get_neighbour(chunks, {0, 1});
+ const auto chunk_backward = this->get_neighbour(chunks, {0, -1});
+ const auto chunk_right = this->get_neighbour(chunks, {1, 0});
+ const auto chunk_left = this->get_neighbour(chunks, {-1, 0});
+ if (!chunk_forward || !chunk_left || !chunk_backward || !chunk_right) {
+ return false;
+ }
+
+ // Single-axis-outside-chunk-bounds-allowed block access.
+ const auto get_outside_block = [&](const int x, const int y,
+ const int z) -> shared::world::block {
+ if (y < 0 || y >= shared::world::chunk::HEIGHT) {
+ return shared::world::block::type::air;
+ } else if (x >= shared::world::chunk::WIDTH) {
+ return chunk_right->get_block({x - WIDTH, y, z});
+ } else if (x < 0) {
+ return chunk_left->get_block({x + WIDTH, y, z});
+ } else if (z >= shared::world::chunk::WIDTH) {
+ return chunk_forward->get_block({x, y, z - WIDTH});
+ } else if (z < 0) {
+ return chunk_backward->get_block({x, y, z + WIDTH});
+ }
+ return this->get_block({x, y, z});
+ };
+
+ // We fill up two vbos, one for each possible rendering pass.
+ std::vector<block::glvert> solid_data;
+ std::vector<block::glvert> water_data;
+
+ // For all blocks in the chunk, check if its neighbours are air. If they
+ // are, it's possible that we can see the block, so add it to vertices.
+ // We need to read into the neighbours chunk occasionally.
+ for (auto x = 0; x < WIDTH; ++x) {
+ for (auto y = 0; y < HEIGHT; ++y) {
+ for (auto z = 0; z < WIDTH; ++z) {
+ const auto& block = this->get_block({x, y, z});
+ const auto bv = shared::world::block::get_visibility(block);
+
+ if (bv == shared::world::block::visibility::invisible) {
+ continue;
+ }
+
+ std::vector<block::glvert> glverts;
+ glverts.reserve(6 * 6);
+
+ const auto draw_type = block::get_draw_type(block.type);
+
+ const block::glfaces_t& faces = block::get_glfaces(block.type);
+ if (draw_type == block::draw_type::block) {
+ const std::array<shared::world::block, 6> around{
+ get_outside_block(x, y, z + 1),
+ get_outside_block(x + 1, y, z),
+ get_outside_block(x, y, z - 1),
+ get_outside_block(x - 1, y, z),
+ get_outside_block(x, y + 1, z),
+ get_outside_block(x, y - 1, z),
+ };
+
+ for (auto i = 0ul; i < std::size(faces); ++i) {
+ const auto ov = block::get_visibility(around[i]);
+ if (bv == block::visibility::translucent &&
+ ov == block::visibility::translucent) {
+ continue;
+ }
+ if (ov == block::visibility::solid) {
+ continue;
+ }
+
+ std::ranges::copy(faces[i],
+ std::back_inserter(glverts));
+ }
+ } else if (draw_type == block::draw_type::custom) {
+ for (const auto& face : faces) {
+ std::ranges::copy(face, std::back_inserter(glverts));
+ }
+ }
+
+ // Move the block pos verts to its intended position.
+ // Move the block texture verts to fit in the atlas.
+ const glm::vec3 offset_vec3{x, y, z};
+ const float tex_yoff = static_cast<float>(block.type) - 1.0f;
+ const auto fix_face = [&](auto& face) {
+ face.vertice += offset_vec3 + 0.5f; // move to origin too
+ face.texture.z += tex_yoff * 6.0f;
+ return face;
+ };
+
+ auto& vbo_dest = block.type == shared::world::block::type::water
+ ? water_data
+ : solid_data;
+ std::ranges::transform(glverts, std::back_inserter(vbo_dest),
+ fix_face);
+ }
+ }
+ }
+
+ const auto generate_vbo = [](const auto& data) -> GLuint {
+ GLuint vbo = 0;
+ glGenBuffers(1, &vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER, std::size(data) * sizeof(block::glvert),
+ std::data(data), GL_STATIC_DRAW);
+ return vbo;
+ };
+ const auto generate_vao = []() -> GLuint {
+ GLuint vao = 0;
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+ // position
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
+ GL_FALSE, sizeof(block::glvert), nullptr);
+ // texture
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
+ GL_FALSE, sizeof(block::glvert),
+ reinterpret_cast<void*>(sizeof(glm::vec3)));
+ return vao;
+ };
+ // If we were to emplace glo with these there is no guarantee that each
+ // function will be called in order (at least, for g++ it isn't). Therefore
+ // we need to call them in order first.
+ const auto solid_vbo = generate_vbo(solid_data);
+ const auto solid_vao = generate_vao();
+ const auto water_vbo = generate_vbo(water_data);
+ const auto water_vao = generate_vao();
+ this->glo.emplace(std::size(solid_data), solid_vbo, solid_vao,
+ std::size(water_data), water_vbo, water_vao);
+ return true;
+}
+
+// http://www.lighthouse3d.com/tutorials/view-frustum-culling/geometric-approach-testing-boxes/
+static bool box_in_frustum(const std::array<glm::vec3, 8>& points) noexcept {
+ const auto& frustum = client::render::camera::get_frustum();
+
+ for (const auto& plane : frustum) {
+ bool inside = false;
+ bool outside = false;
+
+ for (const auto& point : points) {
+ const float distance = plane.x * point.x + plane.y * point.y +
+ plane.z * point.z + plane.w;
+ if (distance < 0.0f) {
+ outside = true;
+ } else {
+ inside = true;
+ }
+
+ if (inside && outside) {
+ break;
+ }
+ }
+
+ if (!inside) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+static bool is_chunk_visible(const float world_x,
+ const float world_z) noexcept {
+ const std::array<glm::vec3, 8> box_vertices =
+ [&world_x, &world_z]() -> std::array<glm::vec3, 8> {
+ const float max_world_x = world_x + shared::world::chunk::WIDTH;
+ const float max_world_z = world_z + shared::world::chunk::WIDTH;
+
+ return {glm::vec3{world_x, 0.0f, world_z},
+ {max_world_x, 0.0f, world_z},
+ {world_x, 0.0f, max_world_z},
+ {max_world_x, 0.0f, max_world_z},
+ {world_x, shared::world::chunk::HEIGHT, world_z},
+ {max_world_x, shared::world::chunk::HEIGHT, world_z},
+ {world_x, shared::world::chunk::HEIGHT, max_world_z},
+ {max_world_x, shared::world::chunk::HEIGHT, max_world_z}};
+ }();
+
+ return box_in_frustum(box_vertices);
+}
+
+bool chunk::draw(const chunks_t& chunks, const shared::player& lp,
+ const pass& pass, const bool skip_regen) noexcept {
+ bool did_regen = false;
+ if (!this->glo.has_value() || this->should_regenerate_vbo) {
+ if (skip_regen || !maybe_regenerate_glo(chunks)) {
+ return false;
+ }
+ this->should_regenerate_vbo = false;
+ did_regen = true;
+ }
+
+ const auto [world_x, world_z] = [&lp, this]() -> std::pair<float, float> {
+ const float offset_x =
+ static_cast<float>(this->pos.x - lp.get_chunk_pos().x);
+ const float offset_z =
+ static_cast<float>(this->pos.z - lp.get_chunk_pos().z);
+ return {offset_x * chunk::WIDTH, offset_z * chunk::WIDTH};
+ }();
+
+ if (is_chunk_visible(world_x, world_z)) {
+ render(world_x, world_z, pass);
+ }
+
+ return did_regen;
+}
+
+} // namespace world
+} // namespace client
diff --git a/src/client/world/chunk.hh b/src/client/world/chunk.hh
new file mode 100644
index 0000000..5436525
--- /dev/null
+++ b/src/client/world/chunk.hh
@@ -0,0 +1,82 @@
+#ifndef CLIENT_WORLD_CHUNK_HH_
+#define CLIENT_WORLD_CHUNK_HH_
+
+#include <algorithm>
+#include <optional>
+#include <ranges>
+#include <unordered_map>
+#include <vector>
+
+#include "client/render/render.hh"
+#include "client/render/texture.hh"
+#include "client/world/block.hh"
+#include "shared/entity/player.hh"
+#include "shared/world/chunk.hh"
+
+namespace client {
+namespace world {
+
+class chunk;
+using chunks_t = std::unordered_map<shared::math::coords,
+ std::optional<client::world::chunk>,
+ decltype(&shared::world::chunk::hash),
+ decltype(&shared::world::chunk::equal)>;
+
+// client::world::chunk is a renderable shared::world::chunk.
+class chunk : public shared::world::chunk {
+public:
+ // Which part to draw when we call draw.
+ enum class pass { solid, water };
+
+public:
+ bool should_regenerate_vbo;
+
+private:
+ struct gl_objects {
+ unsigned long solid_elements;
+ GLuint solid_vbo;
+ GLuint solid_vao;
+ unsigned long water_elements;
+ GLuint water_vbo;
+ GLuint water_vao;
+
+ gl_objects(const unsigned long se, const GLuint svbo, const GLuint svao,
+ const unsigned long we, const GLuint wvbo, const GLuint wvao)
+ : solid_elements(se), solid_vbo(svbo), solid_vao(svao),
+ water_elements(we), water_vbo(wvbo), water_vao(wvao) {}
+ gl_objects(const gl_objects&) = delete;
+ gl_objects(gl_objects&&) = delete;
+ ~gl_objects() {
+ glDeleteBuffers(1, &solid_vbo);
+ glDeleteVertexArrays(1, &solid_vao);
+ glDeleteBuffers(1, &water_vbo);
+ glDeleteVertexArrays(1, &water_vao);
+ }
+ };
+ std::optional<gl_objects> glo;
+
+private:
+ const chunk* get_neighbour(const chunks_t& chunks,
+ shared::math::coords offset) const noexcept;
+
+ void render(const float x_offset, const float z_offset,
+ const pass& pass) noexcept;
+ bool maybe_regenerate_glo(const chunks_t& chunks) noexcept;
+
+public:
+ template <typename... Args>
+ chunk(Args&&... args) noexcept
+ : shared::world::chunk(std::forward<Args>(args)...) {}
+ virtual ~chunk() noexcept = default;
+
+ // true if we regen'd, false otherwise
+ bool draw(const chunks_t& chunks, const shared::player& lp,
+ const pass& pass, const bool skip_regen = false) noexcept;
+
+ bool can_draw() const noexcept { return this->glo.has_value(); }
+};
+
+} // namespace world
+} // namespace client
+
+#endif
diff --git a/src/client/world/world.dat b/src/client/world/world.dat
new file mode 100644
index 0000000..e210d11
--- /dev/null
+++ b/src/client/world/world.dat
Binary files differ