#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 { 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 solid_data; std::vector 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 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 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(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(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& 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 box_vertices = [&world_x, &world_z]() -> std::array { 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 { const float offset_x = static_cast(this->pos.x - lp.get_chunk_pos().x); const float offset_z = static_cast(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