From 1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df Mon Sep 17 00:00:00 2001 From: Nicolas James Date: Wed, 12 Feb 2025 18:05:18 +1100 Subject: initial commit --- src/client/world.cc | 429 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 src/client/world.cc (limited to 'src/client/world.cc') 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 { + 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::make_glfaces(const glface_args& args) noexcept { + + static constexpr std::array 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 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 { + 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 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; + } + + 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 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(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(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); +} + +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 { + const float offset_x = static_cast(this->pos.x - lp.chunk_pos.x); + const float offset_z = static_cast(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 -- cgit v1.2.3