openGL series article directory
preface
Complex 3D models, such as personas in video games or computer-generated movies, are usually generated using modeling tools. This "DCC" (digital content creation) tool enables people (such as artists) to build arbitrary shapes in 3D space and automatically generate vertices, texture coordinates, vertex normal vectors, etc. There are too many such tools to list here. Several examples are 3dsMax, MAYA, Blender, Lightwave, Cinema4D, etc. 3dsMax screen example.
1, obj format 3D model
OBJ file is very simple, and we can relatively easily develop a basic importer. In the OBJ file, the vertex geometric data, texture coordinates, normal vectors and other information are specified in the form of text lines. It has some limitations -- for example, OBJ files cannot specify model animation. A line in an OBJ file that begins with a character mark, indicating the data type on the line. Some common labels include:
v-geometric (vertex position) data;
vt texture coordinates;
vn vertex normal vector;
f-face (usually the vertex in a triangle).
There are other labels that can be used to store object names, materials used, curves, shadows, and many other details
2, obj file format
2. Read in data
The red values (starting with "vt") are the various texture coordinates. The reason why the texture coordinate list is longer than the vertex list is that some vertices participate in multiple triangles, and different texture coordinates may be used in these cases. The green values (starting with "vn") are various normal vectors. The list is also generally longer than the vertex list (although not in this example), also because some vertices participate in multiple triangles and may use different normal vectors in those cases.
A value marked purple near the bottom of the file (starting with "f") specifies a triangle (i.e. "face"). In this example, each face (triangle) has 3 elements, and each element has 3 values separated by "/" (OBJ also allows other formats). The values of each element are the vertex list, texture coordinates, and the index of the normal vector. For example, the third face is:
f 2 / 7 / 3 5 / 8 / 3 3 / 9 / 3
This indicates that the 2nd, 5th, and 3rd vertices (blue) in the vertex list form a triangle (Note:
OBJ index starts from 1). The corresponding texture coordinates are items 7, 8 and 9 in the texture coordinate list in the red section
Item. All 3 vertices have the same normal vector, which is the third item in the list of normal vectors displayed in green.
OBJ format models do not require normal vectors or even texture coordinates. If the model has no texture coordinates or normal vectors,
The value of the face specifies only the vertex index:
f 2 5 3
If the model has texture coordinates but no normal vector, the format is as follows:
f 2 / 7 5 / 8 3 / 9
Also, if the model has a normal vector but no texture coordinates, the format is:
f 2 / / 3 5 / / 3 3 / / 3
It is not uncommon for a model to have tens of thousands of vertices. Almost all conceivable application scenarios can be on the Internet
Download hundreds of such models, including animals, buildings, cars, planes, mythical creatures, people, etc.
Complex programs that can import OBJ models can be obtained on the Internet, and different import programs can be obtained. Write a non
The often simple OBJ loader function is not difficult. It can handle the basic tags we see (v, vt, vn and f).
obj file parsing
Header file
#pragma once #include "glm/glm.hpp" #include "glm/gtc/matrix_transform.hpp" #include "glm/gtc/type_ptr.hpp" #include <iostream> #include <fstream> #include <istream> #include <string> #include <vector> using namespace std; class ImportedModel { private: int _numVertices; //Total number of all vertex coordinates std::vector<glm::vec3> _vertices; //Number of all vertices, including (x,y,z) std::vector<glm::vec2> _texCoords; //Texture coordinates (u, v) std::vector<glm::vec3> _normalVecs; //normal public: ImportedModel(); ImportedModel(const char* filePath); int getNumVertices(); std::vector<glm::vec3> getVertices(); std::vector<glm::vec2> getTextureCoords(); std::vector<glm::vec3> getNormals(); }; class ModelImporter { private: std::vector<float> _vertVals; std::vector<float> _triangleVerts; std::vector<float> _textureCoords; std::vector<float> _stVals; std::vector<float> _normals; std::vector<float> _normVals; public: ModelImporter(); void parseOBJ(const char* filePath); int getNumVertices(); std::vector<float> getVertices(); std::vector<float> getTextureCoordinates(); std::vector<float> getNormals(); };
Implementation file
#include "ImportedModel.h" #include <sstream> ImportedModel::ImportedModel() { } ImportedModel::ImportedModel(const char* filePath) { ModelImporter modelImporter = ModelImporter(); modelImporter.parseOBJ(filePath); _numVertices = modelImporter.getNumVertices(); vector<float> verts = modelImporter.getVertices(); vector<float> tcs = modelImporter.getTextureCoordinates(); vector<float> normals = modelImporter.getNormals(); for (int i=0; i<_numVertices; i++) { _vertices.push_back(glm::vec3(verts[i * 3 + 0], verts[i * 3 + 1], verts[i * 3 + 2])); _texCoords.push_back(glm::vec2(tcs[i * 2 + 0], tcs[i * 2 + 1])); _normalVecs.push_back(glm::vec3(normals[i * 3 + 0], normals[i * 3 + 1], normals[i * 3 + 2])); } } int ImportedModel::getNumVertices() { return _numVertices; } std::vector<glm::vec3> ImportedModel::getVertices() { return _vertices; } std::vector<glm::vec2> ImportedModel::getTextureCoords() { return _texCoords; } std::vector<glm::vec3> ImportedModel::getNormals() { return _normalVecs; } /// <summary> /// ModelImporter implement /// </summary> ModelImporter::ModelImporter() { } void ModelImporter::parseOBJ(const char* filePath) { float x = 0.f, y = 0.f, z = 0.f; string content; ifstream fileStream(filePath, ios::in); string line = ""; while (!fileStream.eof()) { getline(fileStream, line); if (line.compare(0, 2, "v ") == 0) //Notice the space after v { std::stringstream ss(line.erase(0, 1)); ss >> x >> y >> z; //ss >> x; ss >> y; ss >> z; _vertVals.push_back(x); _vertVals.push_back(y); _vertVals.push_back(z); } if (line.compare(0, 2, "vt") == 0) { std::stringstream ss(line.erase(0, 2)); ss >> x >> y; _stVals.push_back(x); _stVals.push_back(y); } if (line.compare(0, 2, "vn") == 0) { std::stringstream ss(line.erase(0, 2)); ss >> x >> y >> z; _normVals.push_back(x); _normVals.push_back(y); _normVals.push_back(z); } if (line.compare(0, 1, "f") == 0) //The original book is wrong { string oneCorner, v, t, n; std::stringstream ss(line.erase(0, 2)); for (int i = 0; i < 3; i++) { getline(ss, oneCorner, ' '); //getline(ss, oneCorner, " "); stringstream oneCornerSS(oneCorner); getline(oneCornerSS, v, '/'); getline(oneCornerSS, t, '/'); getline(oneCornerSS, n, '/'); int vertRef = (stoi(v) - 1) * 3; //Why - 1? int tcRef = (stoi(t) - 1) * 2; int normRef = (stoi(n) - 1) * 3; _triangleVerts.push_back(_vertVals[vertRef]); _triangleVerts.push_back(_vertVals[vertRef + 1]); _triangleVerts.push_back(_vertVals[vertRef + 2]); _textureCoords.push_back(_stVals[tcRef]); _textureCoords.push_back(_stVals[tcRef + 1]); _normals.push_back(_normVals[normRef]); _normals.push_back(_normVals[normRef + 1]); _normals.push_back(_normVals[normRef + 2]); } } } } int ModelImporter::getNumVertices() { return (_triangleVerts.size() / 3); } std::vector<float> ModelImporter::getVertices() { return _triangleVerts; } std::vector<float> ModelImporter::getTextureCoordinates() { return _textureCoords; } std::vector<float> ModelImporter::getNormals() { return _normals; }
obj rendering
#include "glew/glew.h" #include "glfw/glfw3.h" #include "glm/glm.hpp" #include "glm/gtc/matrix_transform.hpp" #include "glm/gtc/type_ptr.hpp" #include "Utils.h" #include "ImportedModel.h" #include "camera.h" #include <iostream> #include <fstream> #include <string> #include <vector> using namespace std; static const int screen_width = 1920; static const int screen_height = 1080; static const int numVAOs = 1; static const int numVBOs = 3; static const float pai = 3.1415926f; GLuint renderingProgram = 0; GLuint vao[numVAOs] = { 0 }; GLuint vbo[numVBOs] = { 0 }; int width = 0; int height = 0; float aspect = 0.f; float objLocX = 0.f, objLocY = 0.f, objLocZ = 0.f; GLuint mvLoc = 0; GLuint projLoc = 0; GLuint shuttleTextureId = 0; glm::mat4 mMat(1.f), vMat(1.f), pMat(1.f), mvMat(1.f); Camera camera(glm::vec3(0.f, 0.f, 2.f)); float cameraX, cameraY, cameraZ; ImportedModel myModel("resources/shuttle.obj"); GLboolean keys[1024] = { GL_FALSE }; GLboolean firstMouse = GL_TRUE; float deltaTime = 0.f; float lastFrame = 0.f; float lastLocX = 0.f; float lastLocY = 0.f; float toRadians(float degrees) { return (degrees * 2.f * pai) / 360.f; } void setupVertices(void) { vector<glm::vec3> vert = myModel.getVertices(); vector<glm::vec2> text = myModel.getTextureCoords(); vector<glm::vec3> norm = myModel.getNormals(); vector<float> pValues; vector<float> tValues; vector<float> nValues; for (int i=0; i< myModel.getNumVertices(); i++) { /*pValues.push_back(vert[i * 3 + 0].x); pValues.push_back(vert[i * 3 + 1].y); pValues.push_back(vert[i * 3 + 2].z); tValues.push_back(text[i * 2 + 0].s); tValues.push_back(text[i * 2 + 1].t); nValues.push_back(norm[i * 3 + 0].x); nValues.push_back(norm[i * 3 + 1].y); nValues.push_back(norm[i * 3 + 2].z);*/ pValues.push_back(vert[i].x); pValues.push_back(vert[i].y); pValues.push_back(vert[i].z); tValues.push_back(text[i].s); tValues.push_back(text[i].t); nValues.push_back(norm[i].x); nValues.push_back(norm[i].y); nValues.push_back(norm[i].z); } glGenVertexArrays(numVAOs, vao); glBindVertexArray(vao[0]); glGenBuffers(numVBOs, vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); glBufferData(GL_ARRAY_BUFFER, pValues.size() * sizeof(float), &(pValues[0]), GL_STATIC_DRAW); //glBufferData(GL_ARRAY_BUFFER, myModel.getVertices().size() * sizeof(float), &(pVlaues[0]), GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); glBufferData(GL_ARRAY_BUFFER, tValues.size() * sizeof(float), &(tValues[0]), GL_STATIC_DRAW); //glBufferData(GL_ARRAY_BUFFER, myModel.getTextureCoords().size() * sizeof(float), &(tValues[0]), GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, vbo[2]); glBufferData(GL_ARRAY_BUFFER, nValues.size() * sizeof(float), &(nValues[0]), GL_STATIC_DRAW); //glBufferData(GL_ARRAY_BUFFER, myModel.getNormals().size() * sizeof(float), &(nValues[0]), GL_STATIC_DRAW); } void init(GLFWwindow* window) { renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl"); //glfwGetWindowSize(window, &width, &height); cameraX = 0.0f; cameraY = 0.0f; cameraZ = 2.f; objLocX = 0.0f; objLocY = 0.0f; objLocZ = 0.0f; glfwGetFramebufferSize(window, &width, &height); aspect = (float)width / (float)height; pMat = glm::perspective(toRadians(45.f), aspect, 0.01f, 1000.f); setupVertices(); shuttleTextureId = Utils::loadTexture("resources/spstob_1.jpg"); /*lastLocX = (float)screen_width / 2.f; lastLocY = (float)screen_height / 2.f;*/ } void window_size_callback(GLFWwindow* window, int newWidth, int newHeight) { //Screen coordinates and frame buffer of window /*GLFW The two coordinate systems in the document are explained here and here. In short, the window coordinates are relative to the monitor and / or window and are given in artificial units that do not necessarily correspond to real screen pixels. This is especially true when DPI scaling is activated (for example, on a Mac with a retinal display). Compared with window coordinates, the size of frame buffer is related to pixels to match the requirements of glViewport OpenGLs. Note that on some systems, window coordinates and pixel coordinates can be the same, but this is not necessarily correct.*/ aspect = (float)newWidth / (float)newHeight; glViewport(0, 0, newWidth, newHeight); cameraX = 0.0f; cameraY = 0.0f; cameraZ =4.f; objLocX = 0.0f; objLocY = 0.0f; objLocZ = 0.0f; mMat = glm::translate(glm::mat4(1.f), glm::vec3(objLocX, objLocY, objLocZ)); //pMat = glm::perspective(glm::radians(45.f), aspect, 0.001f, 1000.f); pMat = glm::perspective(camera.Zoom, aspect, 0.001f, 1000.f); } void do_movement(void) { if (keys[GLFW_KEY_W]) { camera.ProcessKeyboard(FORWARD, deltaTime); } if (keys[GLFW_KEY_S]) { camera.ProcessKeyboard(BACKWARD, deltaTime); } if (keys[GLFW_KEY_A]) { camera.ProcessKeyboard(LEFT, deltaTime); } if (keys[GLFW_KEY_D]) { camera.ProcessKeyboard(RIGHT, deltaTime); } } void display(GLFWwindow* window, double currentTime) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0.1f, 0.5f, 0.5f, 1.f); //Start the shader program and install the GLSL code on the GPU, which will not run the shader program, glUseProgram(renderingProgram); deltaTime = currentTime - lastFrame; lastFrame = currentTime; do_movement(); //Get the sequence number of the position of the uniform variable in the shader program. Through this sequence number, you can set the value of the consistent variable. If there is no variable, return - 1 mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix"); projLoc = glGetUniformLocation(renderingProgram, "proj_matrix"); mMat = glm::translate(glm::mat4(1.f), glm::vec3(objLocX, objLocY, objLocZ)); //vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ)); vMat = camera.GetViewMatrix(); mMat = glm::rotate(mMat, toRadians(0.f), glm::vec3(1.f, 0.f, 0.f)); mMat = glm::rotate(mMat, toRadians(35.f), glm::vec3(0.f, 1.f, 0.f)); mMat = glm::rotate(mMat, toRadians(35.f), glm::vec3(0.f, 0.f, 1.f)); //This sentence must be, or the mouse will fail pMat = glm::perspective(camera.Zoom, aspect, 0.01f, 1000.f); mvMat = vMat * mMat; //Change the value of a uniform matrix variable or array. The location of the uniform variable to be changed is specified by location, and the value of location should be returned by the glGetUniformLocation function // Copy the perspective matrix and MV matrix to the corresponding unified variables /*Pass the consistent variable value into the rendering pipeline through the consistent variable (uniform decorated variable) reference. location : uniform The location of the. count : The number of array elements that need to load data or the number of matrices that need to be modified. transpose : Indicates whether the matrix is a column major matrix (GL_FALSE) or a row major matrix (GL_TRUE). value : Pointer to an array of count elements. */ glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat)); glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat)); //GL_ The false parameter cannot be wrong, otherwise the obj model cannot be displayed glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, shuttleTextureId); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); //Specifies the data format and location of the vertex attribute array whose index value is index at render time /*Parameters index Specifies the index value of the vertex attribute to modify size Specifies the number of components per vertex attribute. Must be 1, 2, 3 or 4. The initial value is 4. (dream dimension: for example, position is composed of 3 (x, y, z) and 4 (r, g, b, a)) type Specifies the data type of each component in the array. The available symbolic constants are GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_FIXED, and GL_FLOAT, the initial value is GL_FLOAT. normalized Specifies whether fixed point data values should be normalized (GL_TRUE) or directly converted to fixed point values (GL_FALSE) when accessed. stride Specifies the offset between successive vertex attributes. If it is 0, the vertex attributes will be understood as: they are closely arranged together. The initial value is 0. pointer Specifies a pointer to the first component of the first vertex attribute in the array. The initial value is 0. */ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); //If enabled, the values in the generic vertex attribute array are accessed and used for rendering when invoking vertex array commands such as glDrawArrays or glDrawElements. //layout(location=0) in vec3 position corresponding to vertex shader; glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, vbo[2]); glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(2); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, shuttleTextureId); glEnable(GL_DEPTH_TEST); //Specifies the comparison value for the depth buffer; glDepthFunc(GL_LEQUAL); glDrawArrays(GL_TRIANGLES, 0, myModel.getNumVertices()); } void press_key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) { if ((key == GLFW_KEY_ESCAPE) && (action == GLFW_PRESS)) { glfwSetWindowShouldClose(window, GLFW_TRUE); } if (action == GLFW_PRESS) { keys[key] = GLFW_TRUE; } else if (action == GLFW_RELEASE) { keys[key] = GLFW_FALSE; } } void mouse_move_callback(GLFWwindow* window, double xPos, double yPos) { if (firstMouse) { lastLocX = xPos; lastLocY = yPos; firstMouse = GL_FALSE; } double offsetLocX = xPos - lastLocX; double offsetLocY = lastLocY - yPos; lastLocX = xPos; lastLocY = yPos; camera.ProcessMouseMovement(offsetLocX, offsetLocY); } void mouse_scroll_callback(GLFWwindow* window, double xPos, double yPos) { camera.ProcessMouseScroll(yPos); } int main(int argc, char** argv) { int glfwState = glfwInit(); if (GLFW_FALSE == glfwState) { cout << "GLFW initialize failed,invoke glfwInit()......Error file:" << __FILE__ << "......Error line:" << __LINE__ << endl; glfwTerminate(); exit(EXIT_FAILURE); } /*Because we want to use OpenGL 4.6, we put GLFW_CONTEXT_VERSION_MAJOR and glfw_ CONTEXT_ VERSION_ The hint corresponding to minor is set to 4 and 6. Because we want to use the OpenGL core mode (which will be mentioned more later), we put glfw_ OPENGL_ The hint corresponding to profile is set to GLFW_OPENGL_CORE_PROFILE, Indicates that OpenGL core mode is used. Finally, glfw_ The hint corresponding to resolvable is set to GLFW_FALSE indicates that the window does not allow users to resize. This is because if the user is allowed to resize, the drawing area of the window will remain unchanged by default (still the area of the original window) after the size changes, In other words, the size and position of the image drawn on the window will not change. To avoid this, we simply don't let users resize the window (Of course, there is a better method, which is to set a callback function of window size with GLFW, but this is relatively simple).*/ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); glfwWindowHint(GLFW_OPENGL_CORE_PROFILE, GLFW_OPENGL_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GL_TRUE); GLFWwindow* window = glfwCreateWindow(screen_width, screen_height, "Load obj file model", nullptr, nullptr); if (!window) { cout << "GLFW create window failed,invoke glfwCreateWindow()......Error line:" << __FILE__ << "......Error line:" << __LINE__ << endl; glfwTerminate(); exit(EXIT_FAILURE); } /*This function makes the OpenGL or OpenGL ES context of the specified window on the calling thread the current context. The context can only be made current on a single thread at a time, and each thread can only have one current context at a time. When moving context between threads, you must first make it non current on the old thread, and then make it current on the new thread. */ glfwMakeContextCurrent(window); glfwSetKeyCallback(window, press_key_callback); glfwSetCursorPosCallback(window, mouse_move_callback); glfwSetScrollCallback(window, mouse_scroll_callback); glfwSetWindowSizeCallback(window, window_size_callback); //Set mouse mode //glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); int glewState = glewInit(); if (GLEW_OK != glewState) { cout << "GLEW initialize failed,invoke glewInit()......Error file:" << __FILE__ << "......Error line:" << __LINE__ << endl; glfwTerminate(); exit(EXIT_FAILURE); } /*This function sets the exchange interval of the current OpenGL or OpenGL ES context, that is, the number of screen updates waiting since calling glfwSwapBuffers, Then swap the buffers and return. This is sometimes referred to as vertical synchronization, vertical flyback synchronization, or vsync only. Support WGL_ EXT_ swap_ control_ Steel and GLX_ EXT_ swap_ control_ The context of the tear extension also accepts negative swap intervals, which allows drivers to swap immediately, Even if the frame arrives a little late. You can check these extensions using glfwExtensionSupported. The context must be up to date on the calling thread. Calling this function without the current context will result in GLFW_NO_CURRENT_CONTEXT error. This feature is not available for Vulkan. If you are rendering with Vulkan, view the current mode of the swap chain instead. */ glfwSwapInterval(1); printf("%s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));//Start initialization process const GLubyte* renderer = glGetString(GL_RENDERER); const GLubyte* vendor = glGetString(GL_VENDOR); const GLubyte* version = glGetString(GL_VERSION); const GLubyte* glslVersion = glGetString(GL_SHADING_LANGUAGE_VERSION); GLint major, minor; glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); printf("GL Vendor : %s\n", vendor); printf("GL Renderer : %s\n", renderer); printf("GL Version (string) : %s\n", version); printf("GL Version (integer) : %d.%d\n", major, minor); printf("GLSL Version : %s\n", glslVersion); glGetError(); // Debug GLEW bug fix /*Because we want to use OpenGL 4.6, we put GLFW_CONTEXT_VERSION_MAJOR and glfw_ CONTEXT_ VERSION_ The hint corresponding to minor is set to 4 and 6. Because we want to use the OpenGL core mode (which will be mentioned more later), we put glfw_ OPENGL_ The hint corresponding to profile is set to GLFW_OPENGL_CORE_PROFILE, Indicates that OpenGL core mode is used. Finally, glfw_ The hint corresponding to resolvable is set to GLFW_FALSE indicates that the window does not allow users to resize. This is because if the user is allowed to adjust the size, the drawing area of the window will remain unchanged by default (still the area of the original window) after the size changes, In other words, the size and position of the image drawn on the window will not change. To avoid this, we simply don't let users resize the window (Of course, there is a better method, which is to set a callback function of window size with GLFW, but this is relatively simple).*/ /*By default, all vertex shader Attribute variables are turned off for performance reasons, It means that the data is invisible on the shader side. Even if the data has been uploaded to the GPU, the specified attribute is enabled by glEnableVertexAttribArray, To access per vertex attribute data in the vertex shader. glVertexAttribPointer or VBO only establishes the logical connection between CPU and GPU, Thus, the CPU data is uploaded to GPU. However, whether the data is visible on the GPU side, that is, whether the shader can read the data, depends on whether the corresponding attribute is enabled, This is the function of glEnableVertexAttribArray, which allows vertex shaders to read GPU (server-side) data. */ int nrAttributes; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl; init(window); while (!glfwWindowShouldClose(window)) { display(window, (float)glfwGetTime()); glfwSwapBuffers(window); glfwPollEvents(); } glfwDestroyWindow(window); glfwTerminate(); exit(EXIT_SUCCESS); return 0; }