#include "client/render/draw.hh" namespace client { namespace render { static void draw_state_info(const shared::player& lp) noexcept { constexpr auto nstr = [](const auto d) -> std::string { auto str = std::to_string(d); str.erase(std::prev(std::end(str), 4), std::end(str)); return str; }; const auto make_position = [&]() -> std::string { const auto chunk_x = lp.get_chunk_pos().x; const auto chunk_z = lp.get_chunk_pos().z; const auto x = static_cast(lp.get_local_pos().x); const auto y = static_cast(lp.get_local_pos().y); const auto z = static_cast(lp.get_local_pos().z); constexpr auto WIDTH = shared::world::chunk::WIDTH; const auto abs_x = static_cast(lp.get_chunk_pos().x) * WIDTH + x; const auto abs_z = static_cast(lp.get_chunk_pos().z) * WIDTH + z; constexpr auto nstr = [](const double d) -> std::string { auto str = std::to_string(d); str.erase(std::prev(std::end(str), 4), std::end(str)); return str; }; const std::string chk_str = '[' + std::to_string(chunk_x) + ", " + std::to_string(chunk_z) + ']'; const std::string abs_str = '(' + nstr(abs_x) + ", " + nstr(y) + ", " + nstr(abs_z) + ')'; return "position: " + abs_str + chk_str; }; const auto make_direction = [&]() -> std::string { const float yaw = glm::degrees(lp.get_angles().yaw); const float pitch = glm::degrees(lp.get_angles().pitch); const std::string bearing = [&]() -> std::string { if (yaw > 135.0f || yaw < -135.0f) { return "South"; } else if (yaw > 45.0f) { return "East"; } else if (yaw > -45.0f) { return "North"; } else { return "West"; } }(); const std::string face_str = '(' + nstr(pitch) + ", " + nstr(yaw) + ')'; // Yaw is between 0 and 360. return "perspective: " + face_str + " -> " + bearing; }; const auto make_address = []() -> std::string { const std::string address = std::string(client::state::address); const std::string port = std::string(client::state::port); return "address: " + address + ":" + port; }; const auto make_tickrate = [&]() -> std::string { return "tickrate: " + std::to_string(state::tickrate); }; const auto make_seed = []() -> std::string { return "seed: " + std::to_string(client::state::seed); }; const auto make_entities = []() -> std::string { const auto index = std::to_string(*client::state::localplayer_index); return "index: " + index; }; const auto make_chunks = []() -> std::string { return "chunks: " + std::to_string(client::state::requested_chunk_count) + ", " + std::to_string(client::state::networked_chunk_count) + " @ " + std::to_string(client::state::draw_distance) + " draw distance"; }; const auto make_speed = [&]() -> std::string { return "speed: " + nstr(glm::length(lp.get_velocity())) + " b/s"; }; const auto make_velocity = [&]() -> std::string { return "velocity: (" + nstr(lp.get_velocity().x) + ", " + nstr(lp.get_velocity().y) + ", " + nstr(lp.get_velocity().z) + ")"; }; const auto make_msg = [&]() -> std::string { const auto& message = get_localplayer().get_message(); return "message: " + (message.has_value() ? message->text : ""); }; const auto make_biome = [&]() -> std::string { const int x = static_cast(lp.get_local_pos().x); const int z = static_cast(lp.get_local_pos().z); const auto is_inside = [](const int val) { return std::clamp(val, 0, shared::world::chunk::WIDTH) == val; }; if (!is_inside(x) || !is_inside(z)) { return "?"; } const auto find_it = client::state::chunks.find(lp.get_chunk_pos()); if (find_it == std::end(client::state::chunks)) { return "?"; } const auto& chunk = find_it->second; if (!chunk.has_value()) { return "?"; } return std::string{"biome: "} + chunk->get_biome(x, z); }; // Draws all of our strings and works its way down the top of the screen. text_args args{.pos = {.extent = {0.0f, 1.0f - 0.015f}}, .extent_height = 0.0165f, .colour = {1.0f, 1.0f, 1.0f, 1.0f}, .has_backing = true}; const auto draw_str_trail = [&args](const std::string& str) { draw_text(str, args); args.pos.extent.y -= 0.02f; }; draw_str_trail("blockgame_linux v0.22"); draw_str_trail(make_address()); draw_str_trail(make_tickrate()); draw_str_trail(make_seed()); draw_str_trail(make_entities()); draw_str_trail(make_chunks()); draw_str_trail(make_position()); draw_str_trail(make_direction()); draw_str_trail(make_speed()); draw_str_trail(make_velocity()); draw_str_trail(make_biome()); draw_str_trail(make_msg()); } static void draw_fps() noexcept { const auto get_fps_colour = [](const int fps) -> glm::vec4 { if (fps == 0) { return {1.0f, 1.0f, 1.0f, 1.0f}; } else if (fps < 30) { return {1.0f, 0.0f, 0.0f, 1.0f}; } else if (fps < 60) { return {1.0f, 0.5f, 0.0f, 1.0f}; } else if (fps < 120) { return {0.0f, 1.0f, 0.0f, 1.0f}; } return {0.0f, 1.0f, 1.0f, 1.0f}; }; const int fps = client::render::get_fps(); const std::string fps_str = fps != 0 ? std::to_string(fps) : "~"; draw_text("FPS: " + fps_str, {.pos = relative_arg{.offset = {0.0f, 2.0f}}, .extent_height = 0.0165f, .colour = get_fps_colour(fps), .has_backing = true}); } // Player position, version, fps etc // Just don't read past this point, doing this is unbelievably ugly. // I don't know why I write this like someone else will read it. static void draw_overlay(const shared::player& lp) noexcept { draw_state_info(lp); draw_fps(); } static void draw_main_pass(const client::player& localplayer, client::entities_t& entities, client::world::chunks_t& chunks) noexcept { { // zero means no limit const unsigned chunks_per_frame = client::settings::get({"video", "chunks_per_frame"}, 0u); unsigned chunks_regenerated = 0u; for (auto& chunk : chunks) { if (!chunk.second.has_value()) { continue; } const bool skip = chunks_per_frame == 0 ? false : chunks_regenerated >= chunks_per_frame; chunks_regenerated += chunk.second->draw( chunks, localplayer, world::chunk::pass::solid, skip); } } for (const auto& [index, entity_ptr] : entities) { if (index == localplayer.get_index()) { continue; } entity_ptr->draw(localplayer); } } static void draw_water_pass(const client::player& localplayer, client::world::chunks_t& chunks) noexcept { for (auto& chunk : chunks) { if (!chunk.second.has_value()) { continue; } chunk.second->draw(chunks, localplayer, client::world::chunk::pass::water); } } static void draw_wts_text(const client::player& localplayer, client::entities_t& entities) noexcept { for (const auto& [index, entity_ptr] : entities) { if (index == localplayer.get_index()) { continue; } entity_ptr->draw_wts(localplayer); } } static void draw_hud(const client::player& localplayer, const client::world::chunks_t& chunks) noexcept { const auto should_draw_hud = !client::input::is_key_toggled(SDLK_F1); if (client::input::is_key_toggled(SDLK_F3) && should_draw_hud) { draw_overlay(localplayer); } if (client::window::is_open()) { return; } if (!should_draw_hud) { return; } // crosshair client::render::draw_rectangle( {.pos = {.extent = {0.5f, 0.5f}, .offset = {-3.0f, -3.0f}}, .size = {.offset = {6.0f, 6.0f}}, .colour = {1.0f, 1.0f, 1.0f, 1.0f}}); // Draw the outline of the block we're aiming at. if (const auto interact = client::movement::interact( localplayer, movement::interact_mode::remove, chunks); interact.has_value()) { const auto [world_x, world_z] = [&]() -> std::pair { const float offset_x = static_cast( interact->first.x - localplayer.get_chunk_pos().x); const float offset_z = static_cast( interact->first.z - localplayer.get_chunk_pos().z); return {offset_x * shared::world::chunk::WIDTH, offset_z * shared::world::chunk::WIDTH}; }(); const glm::vec3 render_pos = { world_x + static_cast(interact->second.x), interact->second.y, world_z + static_cast(interact->second.z)}; client::render::render_cube_outline(render_pos + 0.5f, {0.8f, 0.8f, 0.8f, 1.0f}); } } static void update_camera(const shared::player& localplayer) noexcept { // approximately encompasses everything we could possibly see // if our zfar is too low, our fog breaks // bit of maths for calculating the max distance we could can see client::render::camera::get_zfar() = std::hypot( static_cast(world::chunk::HEIGHT), float(client::state::draw_distance) * std::hypot(16.0f, 16.0f)); client::render::camera::get_pos() = localplayer.get_local_pos() + glm::vec3{0.0f, shared::player::EYE_HEIGHT, 0.0f}; client::render::camera::get_front() = localplayer.get_angles().to_dir(); client::render::camera::update(); } // We create framebuffer here to enable postprocessing and solve some // rendering problems. static void draw_buffers(const client::player& localplayer, client::entities_t& entities, client::world::chunks_t& chunks) noexcept { const auto make_vbo = [](const auto& vertices) -> GLuint { GLuint vbo = 0; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), std::data(vertices), GL_STATIC_DRAW); return vbo; }; const auto make_vao = []() -> GLuint { GLuint vao = 0; glGenVertexArrays(1, &vao); glBindVertexArray(vao); // position glVertexAttribPointer(0, sizeof(glm::vec2) / sizeof(float), GL_FLOAT, GL_FALSE, sizeof(glm::vec2), nullptr); glEnableVertexAttribArray(0); return vao; }; const auto make_ebo = [](const auto& indices) -> GLuint { GLuint ebo = 0; glGenBuffers(1, &ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices, GL_STATIC_DRAW); return ebo; }; const auto make_fbo = []() -> GLuint { GLuint fbo; glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); return fbo; }; const auto make_texture = [](const auto& internal_format, const auto& format, const int texture_offset) -> GLuint { GLuint texture; glGenTextures(1, &texture); glActiveTexture(GL_TEXTURE2 + texture_offset); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); const glm::vec2& window = client::render::get_window_size(); glTexImage2D(GL_TEXTURE_2D, 0, internal_format, window.x, window.y, 0, internal_format, GL_UNSIGNED_BYTE, nullptr); glFramebufferTexture2D(GL_FRAMEBUFFER, format, GL_TEXTURE_2D, texture, 0); return texture; }; static constexpr std::array vertices = { glm::vec2{-1.0f, -1.0f}, glm::vec2{1.0f, -1.0f}, glm::vec2{1.0f, 1.0f}, glm::vec2{-1.0f, 1.0f}}; static constexpr std::array indices = {0, 1, 2, 2, 3, 0}; static const client::render::program framebuffer_program{ "res/shaders/framebuffer.vs", "res/shaders/framebuffer.fs"}; static const GLuint vbo [[maybe_unused]] = make_vbo(vertices); static const GLuint vao = make_vao(); static const GLuint ebo [[maybe_unused]] = make_ebo(indices); // Main frame buffer object. static const GLuint fbo_main = make_fbo(); static const GLuint main_colour [[maybe_unused]] = make_texture(GL_RGB, GL_COLOR_ATTACHMENT0, 0); static const GLuint main_depth [[maybe_unused]] = make_texture(GL_DEPTH_COMPONENT, GL_DEPTH_ATTACHMENT, 1); // Water frame buffer object. static const GLuint fbo_water = make_fbo(); static const GLuint water_colour [[maybe_unused]] = make_texture(GL_RGB, GL_COLOR_ATTACHMENT0, 2); static const GLuint water_depth [[maybe_unused]] = make_texture(GL_DEPTH_COMPONENT, GL_DEPTH_ATTACHMENT, 3); static const GLint u_is_underwater = glGetUniformLocation(framebuffer_program, "_u_is_underwater"); // We render our main scene on the main framebuffer. glBindFramebuffer(GL_FRAMEBUFFER, fbo_main); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); draw_main_pass(localplayer, entities, chunks); // We render our water scene on the water framebuffer. glBindFramebuffer(GL_FRAMEBUFFER, fbo_water); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); const bool is_underwater = [&]() -> bool { const glm::ivec3 pos{client::render::camera::get_pos()}; const auto find_it = chunks.find(localplayer.get_chunk_pos()); if (find_it == std::end(chunks) || !find_it->second.has_value()) { return false; } if (shared::world::chunk::is_outside_chunk(pos)) { return false; } const auto block = find_it->second->get_block(pos); return block.type == shared::world::block::type::water; }(); if (is_underwater) { glFrontFace(GL_CW); draw_water_pass(localplayer, chunks); glFrontFace(GL_CCW); } else { draw_water_pass(localplayer, chunks); } glBindFramebuffer(GL_FRAMEBUFFER, main_colour); glDisable(GL_DEPTH_TEST); glUseProgram(framebuffer_program); glBindVertexArray(vao); glUniform1i(u_is_underwater, is_underwater); glDrawElements(GL_TRIANGLES, std::size(indices), GL_UNSIGNED_INT, nullptr); static client::render::program postprocess_program{ "res/shaders/framebuffer.vs", "res/shaders/postprocess.fs"}; glBindFramebuffer(GL_FRAMEBUFFER, 0); glUseProgram(postprocess_program); glDrawElements(GL_TRIANGLES, std::size(indices), GL_UNSIGNED_INT, nullptr); glEnable(GL_DEPTH_TEST); } void draw(entities_t& entities, world::chunks_t& chunks) noexcept { const auto& localplayer = get_localplayer(); update_camera(localplayer); client::render::update_uniforms(); draw_buffers(localplayer, entities, chunks); draw_hud(localplayer, chunks); draw_wts_text(localplayer, entities); client::window::draw(); client::render::swap_window(); } } // namespace render } // namespace client