aboutsummaryrefslogtreecommitdiff
path: root/src/client/world.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/world.cc')
-rw-r--r--src/client/world.cc429
1 files changed, 429 insertions, 0 deletions
diff --git a/src/client/world.cc b/src/client/world.cc
new file mode 100644
index 0000000..1ceb9fb
--- /dev/null
+++ b/src/client/world.cc
@@ -0,0 +1,429 @@
+#include "world.hh"
+
+namespace client {
+namespace world {
+
+// Additional sanity checks for our atlas.
+static void check_atlas(const client::render::texture& texture) {
+ if (texture.width % 6) {
+ throw std::runtime_error("invalid atlas; WIDTH is not divisible by 6");
+ }
+ if (texture.height % (texture.width / 6)) {
+ throw std::runtime_error(
+ "invalid atlas, HEIGHT is not divisible by (WIDTH / 6)");
+ }
+}
+
+void chunk::render(const float world_x, const float world_z,
+ const pass& pass) noexcept {
+ const auto make_texture = []() -> GLuint {
+ GLuint texture = 0;
+ glActiveTexture(GL_TEXTURE1);
+ glGenTextures(1, &texture);
+ glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
+
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S,
+ GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T,
+ GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
+ GL_LINEAR_MIPMAP_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, 16.0f);
+
+ const client::render::texture stbi{"res/textures/atlas.png"};
+ check_atlas(stbi);
+ const int face_size = stbi.width / 6;
+
+ // 2D texture array, where our depth is our block face.
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, face_size, face_size,
+ 6 * (stbi.height / face_size), 0,
+ stbi.channels == 3 ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+
+ // Fill the 2D texture array.
+ // Because our image has multiple images on the x-axis and opengl
+ // expects a single image per axis, we must fill it in row by row.
+ const auto get_pixel_xy = [&stbi](const int x, const int y) {
+ return stbi.image + 4 * (y * stbi.width + x);
+ };
+ for (int x = 0; x < 6; ++x) {
+ const int x_pixel = x * face_size;
+
+ for (int y = 0; y < stbi.height / face_size; ++y) {
+ const int y_pixel = y * face_size;
+
+ for (auto row = 0; row < face_size; ++row) {
+ glTexSubImage3D(
+ GL_TEXTURE_2D_ARRAY, 0, 0, row, x + y * 6, face_size, 1,
+ 1, GL_RGBA, GL_UNSIGNED_BYTE,
+ get_pixel_xy(x_pixel, row + y_pixel)); // pixel
+ }
+ }
+ }
+
+ glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
+
+ return texture;
+ };
+ 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 GLuint texture [[maybe_unused]] = make_texture();
+ static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix");
+
+ 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);
+}
+
+// This function translates and rotates a set of vertices that describes a
+// neutral face and also adds a vector to the texture coords.
+std::array<chunk::glface, 6>
+chunk::make_glfaces(const glface_args& args) noexcept {
+
+ static constexpr std::array<glface, 6> glfaces = {
+ glface{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}},
+ glface{{0.5f, -0.5f, 0.0f}, {1.0f, 1.0f, 0.0f}},
+ glface{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+ glface{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+ glface{{-0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 0.0f}},
+ glface{{-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.
+ const glm::mat4 mtranslate =
+ args.translate == glm::vec3{}
+ ? glm::mat4{1.0f}
+ : glm::translate(glm::mat4{1.0f}, args.translate);
+ const glm::mat4 mrotate =
+ args.rotate_axis == glm::vec3{}
+ ? glm::mat4{1.0f}
+ : glm::rotate(glm::mat4{1.0f}, glm::radians(args.rotate_degrees),
+ args.rotate_axis);
+
+ std::array<glface, 6> ret;
+
+ std::ranges::transform(glfaces, std::begin(ret), [&](const auto f) {
+ auto face = f; // unfortunate copy
+ face.vertice =
+ glm::vec3(mtranslate * mrotate * glm::vec4{face.vertice, 1.0f});
+ face.texture += args.texture_offset;
+ return face;
+ });
+
+ return ret;
+}
+
+const chunk* chunk::get_neighbour(const chunk::map& 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 chunk::map& chunks) noexcept {
+ // We need all surrounding chunks to make our vbo, this is why it's called
+ // "maybe" regenerate vbo.
+ 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;
+ }
+
+ static const auto [atlas_width,
+ atlas_height] = []() -> std::pair<int, int> {
+ const client::render::texture texture{"res/textures/atlas.png"};
+ check_atlas(texture);
+ return {texture.width, texture.height};
+ }();
+
+ // 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<glface> solid_data;
+ std::vector<glface> 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;
+ }
+
+ const auto& front{get_outside_block(x, y, z + 1)};
+ const auto& back{get_outside_block(x, y, z - 1)};
+ const auto& right{get_outside_block(x + 1, y, z)};
+ const auto& left{get_outside_block(x - 1, y, z)};
+ const auto& up{get_outside_block(x, y + 1, z)};
+ const auto& down{get_outside_block(x, y - 1, z)};
+
+ std::vector<glface> glfaces;
+ glfaces.reserve(6 * 6);
+
+ const auto should_draw_face = [&bv](const auto& other) -> bool {
+ const auto ov = shared::world::block::get_visibility(other);
+ if (bv == shared::world::block::visibility::translucent &&
+ ov == shared::world::block::visibility::translucent) {
+ return false;
+ }
+ return ov != shared::world::block::visibility::solid;
+ };
+ // Special shrub block case, ugly I know.
+ if (block.type == shared::world::block::type::shrub ||
+ block.type == shared::world::block::type::dead_shrub ||
+ block.type == shared::world::block::type::snowy_shrub) {
+ static const auto front_shrub =
+ make_glfaces({.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}});
+ static const auto right_shrub =
+ make_glfaces({.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}});
+ static const auto back_shrub =
+ make_glfaces({.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}});
+ static const auto left_shrub =
+ make_glfaces({.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}});
+
+ std::ranges::copy(front_shrub, std::back_inserter(glfaces));
+ std::ranges::copy(right_shrub, std::back_inserter(glfaces));
+ std::ranges::copy(back_shrub, std::back_inserter(glfaces));
+ std::ranges::copy(left_shrub, std::back_inserter(glfaces));
+
+ } else {
+ if (should_draw_face(front)) {
+ static const auto front_faces = make_glfaces(
+ {.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}});
+ std::ranges::copy(front_faces,
+ std::back_inserter(glfaces));
+ }
+ if (should_draw_face(right)) {
+ static const auto right_faces = make_glfaces(
+ {.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}});
+ std::ranges::copy(right_faces,
+ std::back_inserter(glfaces));
+ }
+ if (should_draw_face(back)) {
+ static const auto back_faces = make_glfaces(
+ {.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}});
+ std::ranges::copy(back_faces,
+ std::back_inserter(glfaces));
+ }
+ if (should_draw_face(left)) {
+ static const auto left_faces = make_glfaces(
+ {.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}});
+ std::ranges::copy(left_faces,
+ std::back_inserter(glfaces));
+ }
+ if (should_draw_face(up)) {
+ static const auto up_faces = make_glfaces(
+ {.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}});
+ std::ranges::copy(up_faces,
+ std::back_inserter(glfaces));
+ }
+ if (should_draw_face(down)) {
+ static const auto down_faces = make_glfaces(
+ {.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}});
+ std::ranges::copy(down_faces,
+ std::back_inserter(glfaces));
+ }
+ }
+
+ // 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 = [&, atlas_width = std::ref(atlas_width),
+ atlas_height =
+ std::ref(atlas_height)](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(glfaces, 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(glface),
+ 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(glface), nullptr);
+ // texture
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT,
+ GL_FALSE, sizeof(glface),
+ 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);
+}
+
+void chunk::draw(const chunk::map& chunks, const shared::player& lp,
+ const pass& pass) noexcept {
+ if (!this->glo.has_value() || this->should_regenerate_vbo) {
+ if (!maybe_regenerate_glo(chunks)) {
+ return;
+ }
+ this->should_regenerate_vbo = false;
+ }
+
+ const auto [world_x, world_z] = [&lp, this]() -> std::pair<float, float> {
+ const float offset_x = static_cast<float>(this->pos.x - lp.chunk_pos.x);
+ const float offset_z = static_cast<float>(this->pos.z - lp.chunk_pos.z);
+ return {offset_x * chunk::WIDTH, offset_z * chunk::WIDTH};
+ }();
+
+ if (!is_chunk_visible(world_x, world_z)) {
+ return;
+ }
+
+ render(world_x, world_z, pass);
+}
+
+} // namespace world
+} // namespace client