aboutsummaryrefslogtreecommitdiff
path: root/src/client/render
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/render')
-rw-r--r--src/client/render/draw.cc428
-rw-r--r--src/client/render/draw.hh30
-rw-r--r--src/client/render/render.cc147
-rw-r--r--src/client/render/render.hh24
-rw-r--r--src/client/render/struct.cc1
-rw-r--r--src/client/render/struct.hh29
6 files changed, 641 insertions, 18 deletions
diff --git a/src/client/render/draw.cc b/src/client/render/draw.cc
new file mode 100644
index 0000000..d9ea9c3
--- /dev/null
+++ b/src/client/render/draw.cc
@@ -0,0 +1,428 @@
+#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<double>(lp.get_local_pos().x);
+ const auto y = static_cast<double>(lp.get_local_pos().y);
+ const auto z = static_cast<double>(lp.get_local_pos().z);
+ constexpr auto WIDTH = shared::world::chunk::WIDTH;
+ const auto abs_x =
+ static_cast<double>(lp.get_chunk_pos().x) * WIDTH + x;
+ const auto abs_z =
+ static_cast<double>(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<int>(lp.get_local_pos().x);
+ const int z = static_cast<int>(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<unsigned>({"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<float, float> {
+ const float offset_x = static_cast<float>(
+ interact->first.x - localplayer.get_chunk_pos().x);
+ const float offset_z = static_cast<float>(
+ 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<float>(interact->second.x),
+ interact->second.y,
+ world_z + static_cast<float>(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<float>(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<glm::vec2, 4> 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<unsigned, 6> 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
diff --git a/src/client/render/draw.hh b/src/client/render/draw.hh
new file mode 100644
index 0000000..bf43224
--- /dev/null
+++ b/src/client/render/draw.hh
@@ -0,0 +1,30 @@
+#ifndef CLIENT_RENDER_DRAW_HH_
+#define CLIENT_RENDER_DRAW_HH_
+
+#include <string_view>
+
+#include <glm/glm.hpp>
+
+#include "client/entity/player.hh"
+#include "client/input.hh"
+#include "client/movement/movement.hh"
+#include "client/render/camera.hh"
+#include "client/render/program.hh"
+#include "client/render/render.hh"
+#include "client/render/struct.hh"
+#include "client/settings.hh"
+#include "client/shared.hh"
+#include "client/window/hud_window.hh"
+#include "client/window/window.hh"
+#include "client/world/chunk.hh"
+
+namespace client {
+namespace render {
+
+void draw(client::entities_t& players,
+ client::world::chunks_t& chunks) noexcept;
+
+} // namespace render
+} // namespace client
+
+#endif
diff --git a/src/client/render/render.cc b/src/client/render/render.cc
index 18aa32a..941988d 100644
--- a/src/client/render/render.cc
+++ b/src/client/render/render.cc
@@ -27,7 +27,7 @@ SDL_Window* const& get_sdl_window() noexcept {
std::bind(SDL_GL_SetAttribute, SDL_GL_CONTEXT_MINOR_VERSION, 2));
SDL_Window* const ret =
- SDL_CreateWindow("blockgame_linux", 0, 0, 0, 0,
+ SDL_CreateWindow("blockgame_linux", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0,
SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN_DESKTOP |
SDL_WINDOW_BORDERLESS);
check_sdl_pointer(ret);
@@ -124,6 +124,71 @@ void swap_window() noexcept {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
+// 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)");
+ }
+}
+
+GLuint get_texture_atlas() noexcept {
+ static const auto atlas = []() -> 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;
+ }();
+ return atlas;
+}
+
void render_cube_outline(const glm::vec3& pos,
const glm::vec4& colour) noexcept {
const auto generate_vbo = [](const auto& vertices) -> GLuint {
@@ -427,6 +492,8 @@ public:
font_atlas(const unsigned size) noexcept
: font_size(size), texture(make_texture_info(size)) {}
~font_atlas() noexcept { glDeleteTextures(1, &this->texture.texture); }
+ font_atlas(font_atlas&&) = delete;
+ font_atlas(const font_atlas&) = delete;
GLint get_height() const noexcept { return this->texture.height; }
GLint get_width() const noexcept { return this->texture.width; }
@@ -578,25 +645,71 @@ void update_uniforms() noexcept {
};
static const GLuint ubo = make_ubo();
- ubo_data upload = {.proj = camera::get_proj(),
- .view = camera::get_view(),
- .frustum = camera::get_frustum(),
- .znear = camera::get_znear(),
- .zfar = camera::get_zfar(),
- .xfov = glm::radians(camera::get_xfov()),
- .yfov = glm::radians(camera::get_yfov()),
- .time = []() -> unsigned {
- static const auto start =
- std::chrono::steady_clock::now();
- const auto now = std::chrono::steady_clock::now();
- return static_cast<unsigned>(
- (now - start) / std::chrono::milliseconds(1));
- }(),
- .xwindow = static_cast<float>(get_window_size().x),
- .ywindow = static_cast<float>(get_window_size().y)};
+ const ubo_data upload = {
+ .proj = camera::get_proj(),
+ .view = camera::get_view(),
+ .frustum = camera::get_frustum(),
+ .znear = camera::get_znear(),
+ .zfar = camera::get_zfar(),
+ .xfov = glm::radians(camera::get_xfov()),
+ .yfov = glm::radians(camera::get_yfov()),
+ .time = []() -> unsigned {
+ static const auto start = std::chrono::steady_clock::now();
+ const auto now = std::chrono::steady_clock::now();
+ return static_cast<unsigned>((now - start) /
+ std::chrono::milliseconds(1));
+ }(),
+ .xwindow = static_cast<float>(get_window_size().x),
+ .ywindow = static_cast<float>(get_window_size().y)};
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(upload), &upload);
}
+void draw_rectangle(const rectangle_args& ra) noexcept {
+ const glm::vec2 pos = ra.pos.to_vec2();
+ const glm::vec2 size = ra.size.to_vec2();
+
+ // We want to render from the bottom left corner.
+ render::render_rectangle(pos + (size / 2.0f), size, ra.colour);
+}
+
+void draw_colour(const glm::vec4& colour) noexcept {
+ const rectangle_args args = {
+ .pos = {.extent = {0.0f, 0.0f}},
+ .size = {.extent = {1.0f, 1.0f}},
+ .colour = colour,
+ };
+ draw_rectangle(args);
+}
+
+void draw_text(const std::string_view text, const text_args& args) noexcept {
+ const glm::vec2& window = render::get_window_size();
+ const float x = floorf(window.x * args.pos.extent.x + args.pos.offset.x);
+ const float y = floorf(window.y * args.pos.extent.y + args.pos.offset.y);
+ const unsigned height = static_cast<unsigned>(
+ args.extent_height * window.y + args.offset_height);
+
+ const auto make_matrix = [&](const float xo, const float yo) -> glm::mat4 {
+ constexpr auto identity = glm::mat4(1.0f);
+ const auto proj = glm::ortho(0.f, window.x, 0.f, window.y);
+ const auto tran =
+ glm::translate(identity, glm::vec3{x + xo, y + yo, 0.0f});
+ return proj * tran;
+ };
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_BLEND);
+ if (args.has_backing) {
+ constexpr glm::vec4 black{0.0f, 0.0f, 0.0f, 1.0f};
+ render_text(text, height, args.is_centered, args.is_vcentered, black,
+ make_matrix(2.0f, -2.0f));
+ }
+ glEnable(GL_BLEND);
+
+ render_text(text, height, args.is_centered, args.is_vcentered, args.colour,
+ make_matrix(0, 0));
+ glEnable(GL_DEPTH_TEST);
+}
+
} // namespace render
} // namespace client
diff --git a/src/client/render/render.hh b/src/client/render/render.hh
index ca98e6f..c98043a 100644
--- a/src/client/render/render.hh
+++ b/src/client/render/render.hh
@@ -27,8 +27,10 @@
#include "client/render/camera.hh"
#include "client/render/program.hh"
+#include "client/render/struct.hh"
+#include "client/render/texture.hh"
#include "shared/shared.hh"
-#include "shared/world.hh"
+#include "shared/world/chunk.hh"
namespace client {
namespace render {
@@ -40,6 +42,7 @@ void quit() noexcept;
int get_fps() noexcept;
SDL_Window* const& get_sdl_window() noexcept;
const glm::ivec2& get_window_size() noexcept;
+GLuint get_texture_atlas() noexcept;
// Update
void update_uniforms() noexcept;
@@ -54,7 +57,26 @@ void render_text(const std::string_view text, const unsigned int size,
void render_cube_outline(const glm::vec3& pos,
const glm::vec4& colour) noexcept;
+// Draw is similar to render but more abstracted.
+void draw_colour(const glm::vec4& colour) noexcept;
+struct rectangle_args {
+ relative_arg pos;
+ relative_arg size;
+ glm::vec4 colour;
+};
+void draw_rectangle(const rectangle_args& args) noexcept;
+
+struct text_args {
+ relative_arg pos;
+ float extent_height; // we don't get width here,
+ float offset_height;
+ glm::vec4 colour;
+ bool has_backing;
+ bool is_centered;
+ bool is_vcentered;
+};
+void draw_text(const std::string_view text, const text_args& args) noexcept;
} // namespace render
} // namespace client
diff --git a/src/client/render/struct.cc b/src/client/render/struct.cc
new file mode 100644
index 0000000..a7a1b5d
--- /dev/null
+++ b/src/client/render/struct.cc
@@ -0,0 +1 @@
+#include "client/render/struct.hh"
diff --git a/src/client/render/struct.hh b/src/client/render/struct.hh
new file mode 100644
index 0000000..fd00ffd
--- /dev/null
+++ b/src/client/render/struct.hh
@@ -0,0 +1,29 @@
+#ifndef CLIENT_RENDER_STRUCT_HH_
+#define CLIENT_RENDER_STRUCT_HH_
+
+//#include <client/render/render.hh>
+#include <glm/glm.hpp>
+
+namespace client {
+namespace render {
+
+const glm::ivec2& get_window_size() noexcept; // forward declaration
+
+// Scale takes the range [0, 1] and represents an amount of the screen.
+// Offset is any value that is added after scale.
+struct relative_arg {
+ glm::vec2 extent;
+ glm::vec2 offset;
+
+ glm::vec2 to_vec2() const noexcept {
+ const glm::vec2& window = render::get_window_size();
+ const float x = this->extent.x * window.x + this->offset.x;
+ const float y = this->extent.y * window.y + this->offset.y;
+ return {x, y};
+ }
+};
+
+} // namespace render
+} // namespace client
+
+#endif