diff --git a/include/chunk.hpp b/include/chunk.hpp index 19a9dc6..2ae8daf 100644 --- a/include/chunk.hpp +++ b/include/chunk.hpp @@ -52,9 +52,6 @@ namespace Chunk ~Chunk(); public: - void createBuffers(); - void deleteBuffers(); - glm::vec3 getPosition() { return this->position; } void setState(chunk_state_t nstate, bool value); bool getState(chunk_state_t n) { return (this->state & n) == n; } @@ -72,7 +69,6 @@ namespace Chunk std::unique_ptr getBlocksArray(int* len) { return (this->blocks.toArray(len)); } public: - GLuint VAO{0}, VBO{0}, extentsBuffer{0}, texinfoBuffer{0}, numVertices{0}; std::atomic unload_timer{0}; chunk_index_t getIndex(){ return this->index; } diff --git a/include/chunkmeshdata.hpp b/include/chunkmeshdata.hpp new file mode 100644 index 0000000..74fd74d --- /dev/null +++ b/include/chunkmeshdata.hpp @@ -0,0 +1,36 @@ +#ifndef CHUNK_MESH_DATA_H +#define CHUNK_MESH_DATA_H + +#include +#include "chunk.hpp" + +enum class ChunkMeshDataType{ + MESH_UPDATE +}; + +typedef struct ChunkMeshData{ + chunk_index_t index; + glm::vec3 position; + int num_vertices = 0; + + std::vector vertices; + std::vector extents; + std::vector texinfo; + + ChunkMeshDataType message_type; + + void clear(){ + vertices.clear(); + texinfo.clear(); + extents.clear(); + index = 0; + position = glm::vec3(0); + num_vertices = 0; + } + +}ChunkMeshData; +typedef oneapi::tbb::concurrent_queue ChunkMeshDataQueue; + + +#endif + diff --git a/include/chunkmesher.hpp b/include/chunkmesher.hpp index ff8a79a..cbfe4f9 100644 --- a/include/chunkmesher.hpp +++ b/include/chunkmesher.hpp @@ -5,10 +5,12 @@ #include #include +#include #include #include #include "chunk.hpp" +#include "chunkmeshdata.hpp" #include "globals.hpp" #include "shader.hpp" @@ -21,13 +23,12 @@ namespace chunkmesher{ std::vector extents; std::vector texinfo; }; - oneapi::tbb::concurrent_queue& getMeshDataQueue(); + ChunkMeshDataQueue& getMeshDataQueue(); + void init(); void mesh(Chunk::Chunk* chunk); - void sendtogpu(MeshData* mesh_data); - void quad(MeshData* mesh_data, glm::vec3 bottomLeft, glm::vec3 topLeft, glm::vec3 topRight, - glm::vec3 bottomRight, glm::vec3 normal, Block block, int dim, bool backFace); } #endif + diff --git a/include/renderer.hpp b/include/renderer.hpp index d772c77..c0cdcba 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -6,12 +6,43 @@ #include "chunk.hpp" #include "chunkmesher.hpp" +#include "chunkmeshdata.hpp" #include "shader.hpp" namespace renderer{ - typedef oneapi::tbb::concurrent_unordered_set RenderSet; + typedef struct RenderInfo { + chunk_index_t index; + int num_vertices; + glm::vec3 position; + bool buffers_allocated=false; + + GLuint VAO, VBO, extentsBuffer, texinfoBuffer; + + void allocateBuffers(){ + // Allocate buffers + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &extentsBuffer); + glGenBuffers(1, &texinfoBuffer); + + buffers_allocated=true; + } + + void deallocateBuffers(){ + // Allocate buffers + glDeleteBuffers(1, &VBO); + glDeleteBuffers(1, &extentsBuffer); + glDeleteBuffers(1, &texinfoBuffer); + glDeleteVertexArrays(1, &VAO); + + buffers_allocated=false; + } + } RenderInfo; + + typedef oneapi::tbb::concurrent_queue IndexQueue; void init(GLFWwindow* window); + void send_chunk_to_gpu(ChunkMeshData* mesh_data, RenderInfo* render_info); void render(); void resize_framebuffer(int width, int height); void framebuffer_size_callback(GLFWwindow *window, int width, int height); @@ -20,9 +51,11 @@ namespace renderer{ void saveScreenshot(bool forceFullHD=false); Shader* getRenderShader(); - RenderSet& getChunksToRender(); - oneapi::tbb::concurrent_queue& getMeshDataQueue(); + ChunkMeshDataQueue& getMeshDataQueue(); + IndexQueue& getDeleteIndexQueue(); + }; #endif + diff --git a/src/chunk.cpp b/src/chunk.cpp index 6eebecd..b85443d 100644 --- a/src/chunk.cpp +++ b/src/chunk.cpp @@ -36,21 +36,6 @@ namespace Chunk { } - void Chunk::createBuffers(){ - glGenVertexArrays(1, &(this->VAO)); - glGenBuffers(1, &(this->VBO)); - glGenBuffers(1, &(this->extentsBuffer)); - glGenBuffers(1, &(this->texinfoBuffer)); - - } - - void Chunk::deleteBuffers(){ - glDeleteBuffers(1, &(this->VBO)); - glDeleteBuffers(1, &(this->extentsBuffer)); - glDeleteBuffers(1, &(this->texinfoBuffer)); - glDeleteVertexArrays(1, &(this->VAO)); - } - Block Chunk::getBlock(int x, int y, int z) { if(x < 0 || y < 0 || z < 0 || x > CHUNK_SIZE -1 || y > CHUNK_SIZE -1 || z > CHUNK_SIZE-1 || diff --git a/src/chunkmanager.cpp b/src/chunkmanager.cpp index 1eb021d..1addbce 100644 --- a/src/chunkmanager.cpp +++ b/src/chunkmanager.cpp @@ -53,10 +53,6 @@ namespace chunkmanager index++; } - // Also init mesh data queue - for(int i = 0; i < 10; i++) - chunkmesher::getMeshDataQueue().push(new chunkmesher::MeshData()); - should_run = true; update_thread = std::thread(update); gen_thread = std::thread(generate); @@ -85,7 +81,6 @@ namespace chunkmanager if(chunks_to_mesh_queue.try_pop(entry)){ Chunk::Chunk* chunk = entry.first; chunkmesher::mesh(chunk); - renderer::getChunksToRender().insert(chunk); chunk->setState(Chunk::CHUNK_STATE_IN_MESHING_QUEUE, false); } } @@ -117,6 +112,7 @@ namespace chunkmanager // Using the key doesn't work if(chunks.erase(a)){ nUnloaded++; + renderer::getDeleteIndexQueue().push(index); delete c; } else { c->setState(Chunk::CHUNK_STATE_IN_DELETING_QUEUE, false); diff --git a/src/chunkmesher.cpp b/src/chunkmesher.cpp index d87a6e2..2d200ad 100755 --- a/src/chunkmesher.cpp +++ b/src/chunkmesher.cpp @@ -11,15 +11,24 @@ #include "spacefilling.hpp" #include "utils.hpp" +#define CHUNK_MESH_DATA_QUANTITY 100 +#define CHUNK_MESH_WORLD_LIMIT_BORDERS 0 + namespace chunkmesher{ -oneapi::tbb::concurrent_queue MeshDataQueue; +ChunkMeshDataQueue MeshDataQueue; -oneapi::tbb::concurrent_queue& getMeshDataQueue(){ return MeshDataQueue; } +ChunkMeshDataQueue& getMeshDataQueue(){ return MeshDataQueue; } + +void init() +{ + for(int i = 0; i < CHUNK_MESH_DATA_QUANTITY; i++) + MeshDataQueue.push(new ChunkMeshData{}); +} void mesh(Chunk::Chunk* chunk) { - MeshData* mesh_data; + ChunkMeshData* mesh_data; if(!MeshDataQueue.try_pop(mesh_data)) return; /* @@ -38,34 +47,27 @@ void mesh(Chunk::Chunk* chunk) */ // Cleanup previous data - mesh_data->numVertices = 0; - mesh_data->chunk = chunk; - mesh_data->vertices.clear(); - mesh_data->extents.clear(); - mesh_data->texinfo.clear(); - - // Abort if chunk is empty - if(chunk->getState(Chunk::CHUNK_STATE_EMPTY)){ - chunk->setState(Chunk::CHUNK_STATE_MESHED, true); - renderer::getMeshDataQueue().push(mesh_data); - return; - } + mesh_data->clear(); + mesh_data->message_type = ChunkMeshDataType::MESH_UPDATE; + mesh_data->index = chunk->getIndex(); + mesh_data->position = chunk->getPosition(); // convert tree to array since it is easier to work with it int length{0}; - std::unique_ptr blocks = chunk->getBlocksArray(&length); - if(length == 0) { - chunk->setState(Chunk::CHUNK_STATE_MESHED, true); - renderer::getMeshDataQueue().push(mesh_data); - return; - } - + std::unique_ptr blocks; + int k, l, u, v, w, h, n, j, i; int x[]{0, 0, 0}; int q[]{0, 0, 0}; int du[]{0, 0, 0}; int dv[]{0, 0, 0}; + // Abort if chunk is empty + if(chunk->getState(Chunk::CHUNK_STATE_EMPTY)) goto end; + + blocks = chunk->getBlocksArray(&length); + if(length == 0) goto end; + std::array mask; for (bool backFace = true, b = false; b != backFace; backFace = backFace && b, b = !b) { @@ -128,9 +130,15 @@ void mesh(Chunk::Chunk* chunk) // The else case provides face culling for adjacent solid faces // Checking for NULLBLK avoids creating empty faces if nearby chunk was not // yet generated +#if CHUNK_MESH_WORLD_LIMIT_BORDERS == 1 mask[n++] = b1 == b2 ? Block::NULLBLK : backFace ? b1 == Block::NULLBLK || b1 == Block::AIR ? b2 : Block::NULLBLK : b2 == Block::NULLBLK || b2 == Block::AIR ? b1 : Block::NULLBLK; +#else + mask[n++] = b1 == b2 ? Block::NULLBLK + : backFace ? b1 == Block::AIR ? b2 : Block::NULLBLK + : b2 == Block::AIR ? b1 : Block::NULLBLK; +#endif } } @@ -191,7 +199,7 @@ void mesh(Chunk::Chunk* chunk) mesh_data->texinfo.push_back(backFace ? 0.0 : 1.0); mesh_data->texinfo.push_back((int)(mask[n]) - 2); - mesh_data->numVertices++; + mesh_data->num_vertices++; } for (l = 0; l < h; ++l) @@ -220,49 +228,8 @@ void mesh(Chunk::Chunk* chunk) } } +end: chunk->setState(Chunk::CHUNK_STATE_MESHED, true); renderer::getMeshDataQueue().push(mesh_data); } - -void sendtogpu(MeshData* mesh_data) -{ - if (mesh_data->numVertices > 0) - { - if(mesh_data->chunk->VAO == 0) mesh_data->chunk->createBuffers(); - - // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). - glBindVertexArray(mesh_data->chunk->VAO); - - // position attribute - glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->VBO); - glBufferData(GL_ARRAY_BUFFER, mesh_data->vertices.size() * sizeof(GLfloat), &(mesh_data->vertices[0]), GL_STATIC_DRAW); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); - glEnableVertexAttribArray(0); - - // normal attribute - glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->extentsBuffer); - glBufferData(GL_ARRAY_BUFFER, mesh_data->extents.size() * sizeof(GLfloat), &(mesh_data->extents[0]), GL_STATIC_DRAW); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)(0)); - glEnableVertexAttribArray(1); - - // texcoords attribute - glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->texinfoBuffer); - glBufferData(GL_ARRAY_BUFFER, mesh_data->texinfo.size() * sizeof(GLfloat), &(mesh_data->texinfo[0]), GL_STATIC_DRAW); - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); - - glBindVertexArray(0); - - // save the number of indices of the mesh, it is needed later for drawing - mesh_data->chunk->numVertices = (GLuint)(mesh_data->numVertices); - - // once data has been sent to the GPU, it can be cleared from system RAM - mesh_data->vertices.clear(); - mesh_data->extents.clear(); - mesh_data->texinfo.clear(); - } - - // mark the chunk mesh has loaded on GPU - mesh_data->chunk->setState(Chunk::CHUNK_STATE_MESH_LOADED, true); -} }; diff --git a/src/debugwindow.cpp b/src/debugwindow.cpp index f6e7977..256dd2e 100644 --- a/src/debugwindow.cpp +++ b/src/debugwindow.cpp @@ -95,18 +95,15 @@ namespace debug{ } if(ImGui::CollapsingHeader("Mesh")){ - ImGui::Text("Total chunks updated: %d", + ImGui::Text("Total chunk meshed: %d", std::any_cast(parameters.at("render_chunks_total"))); + ImGui::Text("Of which renderable (not empty): %d", + std::any_cast(parameters.at("render_chunks_renderable"))); ImGui::Text("Chunks rendered: %d", std::any_cast(parameters.at("render_chunks_rendered"))); ImGui::Text("Frustum culled: %d", std::any_cast(parameters.at("render_chunks_culled"))); - ImGui::Text("Chunks out of view: %d", - std::any_cast(parameters.at("render_chunks_oof"))); - if(parameters.find("render_chunks_deleted") != parameters.end()) - ImGui::Text("Chunks deleted: %d", - std::any_cast(parameters.at("render_chunks_deleted"))); - ImGui::Text("Vertices in the scene: %d", + ImGui::Text("Total vertices in the scene: %d", std::any_cast(parameters.at("render_chunks_vertices"))); ImGui::Checkbox("Wireframe", std::any_cast(parameters.at("wireframe_return"))); diff --git a/src/main.cpp b/src/main.cpp index 6aafb51..7bc51f9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -69,6 +69,7 @@ int main() SpaceFilling::initLUT(); controls::init(); chunkmanager::init(); + chunkmesher::init(); debug::window::init(window); renderer::init(window); diff --git a/src/renderer.cpp b/src/renderer.cpp index 0dcd3d2..5daa398 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -1,7 +1,8 @@ #include "renderer.hpp" -#include -#include +#include +#include +#include #include "chunkmanager.hpp" #include "chunkmesher.hpp" @@ -12,16 +13,18 @@ #include "stb_image_write.h" namespace renderer{ - RenderSet chunks_torender; - oneapi::tbb::concurrent_vector render_todelete; - oneapi::tbb::concurrent_queue MeshDataQueue; + typedef oneapi::tbb::concurrent_hash_map RenderTable; + + RenderTable ChunksToRender; + ChunkMeshDataQueue MeshDataQueue; + IndexQueue MeshDataToDelete; Shader* theShader, *quadShader; GLuint chunkTexture; Shader* getRenderShader() { return theShader; } - RenderSet& getChunksToRender(){ return chunks_torender; } - oneapi::tbb::concurrent_queue& getMeshDataQueue(){ return MeshDataQueue; } + ChunkMeshDataQueue& getMeshDataQueue(){ return MeshDataQueue; } + IndexQueue& getDeleteIndexQueue(){ return MeshDataToDelete; } GLuint renderTexFrameBuffer, renderTex, renderTexDepthBuffer, quadVAO, quadVBO; int screenWidth, screenHeight; @@ -138,98 +141,95 @@ namespace renderer{ theShader->use(); theShader->setVec3("viewPos", cameraPos); - chunkmesher::MeshData* m; + /* Process incoming mesh data */ + ChunkMeshData* m; while(MeshDataQueue.try_pop(m)){ - //chunkmesher::sendtogpu(m); + RenderTable::accessor a; + RenderInfo* render_info; + + if(ChunksToRender.find(a, m->index)){ + render_info = a->second; + render_info->position = m->position; + render_info->num_vertices = m->num_vertices; + }else{ + render_info = new RenderInfo(); + render_info->index = m->index; + render_info->position = m->position; + render_info->num_vertices = m->num_vertices; + + ChunksToRender.emplace(a, std::make_pair(render_info->index, render_info)); + } + + send_chunk_to_gpu(m, render_info); chunkmesher::getMeshDataQueue().push(m); } - /* - for(auto& c : chunks_torender){ - float dist = glm::distance(c->getPosition(), cameraChunkPos); - if(dist <= static_cast(RENDER_DISTANCE)){ - if(!c->getState(Chunk::CHUNK_STATE_MESH_LOADED)) continue; + /* Process chunks to be removed */ + chunk_index_t queue_index; + while(MeshDataToDelete.try_pop(queue_index)){ + RenderTable::accessor a; - if(c->numVertices > 0){ - // Increase total vertex count - vertices += c->numVertices; - - // reset out-of-vision and unload flags - c->setState(Chunk::CHUNK_STATE_OUTOFVISION, false); - c->setState(Chunk::CHUNK_STATE_UNLOADED, false); - - // Perform frustum culling and eventually render - glm::vec3 chunk = c->getPosition(); - glm::vec4 chunkW = glm::vec4(chunk.x*static_cast(CHUNK_SIZE), chunk.y*static_cast(CHUNK_SIZE), chunk.z*static_cast(CHUNK_SIZE),1.0); - glm::mat4 model = glm::translate(glm::mat4(1.0), ((float)CHUNK_SIZE) * chunk); - - // Check if all the corners of the chunk are outside any of the planes - // TODO (?) implement frustum culling as per (Inigo Quilez)[https://iquilezles.org/articles/frustumcorrect/], and check each - // plane against each corner of the chunk - bool out=false; - int a{0}; - for(int p = 0; p < 6; p++){ - a = 0; - for(int i = 0; i < 8; i++) a += glm::dot(frustumPlanes[p], glm::vec4(chunkW.x + ((float)(i & 1))*CHUNK_SIZE, chunkW.y - + ((float)((i & 2) >> 1))*CHUNK_SIZE, chunkW.z + ((float)((i & 4) >> 2))*CHUNK_SIZE, 1.0)) < 0.0; - - if(a==8){ - out=true; - break; - } - } - - if (!out) - { - theShader->setMat4("model", model); - theShader->setMat4("view", theCamera.getView()); - theShader->setMat4("projection", theCamera.getProjection()); - - glBindVertexArray(c->VAO); - glDrawArrays(GL_POINTS, 0, c->numVertices); - glBindVertexArray(0); - - toGpu++; - } - } - }else{ - // When the chunk is outside render distance - - if(c->getState(Chunk::CHUNK_STATE_OUTOFVISION)){ - oof++; - if(glfwGetTime() - c->unload_timer > UNLOAD_TIMEOUT){ - // If chunk was already out and enough time has passed - // Mark the chunk to be unloaded - // And mark is to be removed from the render set - render_todelete.push_back(c); - } - } else{ - // Mark has out of vision and annotate when it started - c->setState(Chunk::CHUNK_STATE_OUTOFVISION, true); - c->setState(Chunk::CHUNK_STATE_UNLOADED, false); - c->unload_timer = glfwGetTime(); - } - + if(ChunksToRender.find(a, queue_index)){ + RenderInfo* render_info = a->second; + render_info->deallocateBuffers(); + delete render_info; + ChunksToRender.erase(a); } } - */ - total = chunks_torender.size(); - debug::window::set_parameter("render_chunks_total", total); - debug::window::set_parameter("render_chunks_rendered", toGpu); - debug::window::set_parameter("render_chunks_culled", total-toGpu); - debug::window::set_parameter("render_chunks_oof", oof); - debug::window::set_parameter("render_chunks_deleted", (int) (render_todelete.size())); - debug::window::set_parameter("render_chunks_vertices", vertices); + /* Render the chunks */ + // parallel_for cannot be used since all the rendering needs to happen in a single thread + for(RenderTable::iterator i = ChunksToRender.begin(); i != ChunksToRender.end(); i++){ + RenderInfo* render_info = i->second; - /*for(auto& c : render_todelete){ - // we can get away with unsafe erase as access to the container is only done by this - // thread - c->deleteBuffers(); - chunks_torender.unsafe_erase(c); - chunkmanager::getDeleteVector().push(c); + if(render_info->num_vertices > 0) + { + total++; + + // Increase total vertex count + vertices += render_info->num_vertices; + + // Perform frustum culling and eventually render + glm::vec3 chunk = render_info->position; + glm::vec4 chunkW = glm::vec4(chunk.x*static_cast(CHUNK_SIZE), chunk.y*static_cast(CHUNK_SIZE), chunk.z*static_cast(CHUNK_SIZE),1.0); + glm::mat4 model = glm::translate(glm::mat4(1.0), ((float)CHUNK_SIZE) * chunk); + + // Check if all the corners of the chunk are outside any of the planes + // TODO (?) implement frustum culling as per (Inigo Quilez)[https://iquilezles.org/articles/frustumcorrect/], and check each + // plane against each corner of the chunk + bool out=false; + int a{0}; + for(int p = 0; p < 6; p++){ + a = 0; + for(int i = 0; i < 8; i++) a += glm::dot(frustumPlanes[p], glm::vec4(chunkW.x + ((float)(i & 1))*CHUNK_SIZE, chunkW.y + + ((float)((i & 2) >> 1))*CHUNK_SIZE, chunkW.z + ((float)((i & 4) >> 2))*CHUNK_SIZE, 1.0)) < 0.0; + + if(a==8){ + out=true; + break; + } + } + + if (!out) + { + theShader->setMat4("model", model); + theShader->setMat4("view", theCamera.getView()); + theShader->setMat4("projection", theCamera.getProjection()); + + glBindVertexArray(render_info->VAO); + glDrawArrays(GL_POINTS, 0, render_info->num_vertices); + glBindVertexArray(0); + + toGpu++; + } + } } - render_todelete.clear();*/ + + debug::window::set_parameter("render_chunks_total", (int)(ChunksToRender.size())); + debug::window::set_parameter("render_chunks_rendered", toGpu); + debug::window::set_parameter("render_chunks_renderable", total); + debug::window::set_parameter("render_chunks_culled", total-toGpu); + debug::window::set_parameter("render_chunks_vertices", vertices); /* DISPLAY TEXTURE ON A QUAD THAT FILLS THE SCREEN */ // Now to render the quad, with the texture on top @@ -252,6 +252,40 @@ namespace renderer{ debug::window::render(); } + void send_chunk_to_gpu(ChunkMeshData* mesh_data, RenderInfo* render_info) + { + if (render_info->num_vertices > 0) + { + if(!render_info->buffers_allocated) render_info->allocateBuffers(); + + // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). + glBindVertexArray(render_info->VAO); + + // TODO: change GL_STATIC_DRAW to the one that means "few redraws and further in between" + + // position attribute + glBindBuffer(GL_ARRAY_BUFFER, render_info->VBO); + glBufferData(GL_ARRAY_BUFFER, mesh_data->vertices.size() * sizeof(GLfloat), &(mesh_data->vertices[0]), GL_STATIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); + glEnableVertexAttribArray(0); + + // normal attribute + glBindBuffer(GL_ARRAY_BUFFER, render_info->extentsBuffer); + glBufferData(GL_ARRAY_BUFFER, mesh_data->extents.size() * sizeof(GLfloat), &(mesh_data->extents[0]), GL_STATIC_DRAW); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)(0)); + glEnableVertexAttribArray(1); + + // texcoords attribute + glBindBuffer(GL_ARRAY_BUFFER, render_info->texinfoBuffer); + glBufferData(GL_ARRAY_BUFFER, mesh_data->texinfo.size() * sizeof(GLfloat), &(mesh_data->texinfo[0]), GL_STATIC_DRAW); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); + + glBindVertexArray(0); + } + } + + void framebuffer_size_callback(GLFWwindow *window, int width, int height){ resize_framebuffer(width, height); }