Skip to content
unholypractices edited this page Feb 8, 2024 · 33 revisions

OpenGL Crash Course (Rendering + Animation)

Use this as your cheat sheet while learning.

In this document we will have the following:

  • Setup our C++ environment
  • Rendering 3D models
    • With bonus Toon Shading and Outlines (cartoon, anime style rendering)
  • Implementing a very basic skeletal animation system
  • Setup our C++ environment

    Rendering 3D models

    We will use the following libraries in order to get this working!

    • GLFW (we use this mostly for creating our window)
    • GLEW (we use this so we get full access to the OpenGL functions)
    • GLM (we use this so we won't need to implement all the math from scratch, really neat library!)
    • ASSIMP (we use this so we can import 3d models and animations, without the need to write our own parser!)
    • STB (we need stb_image.h and stb_image_resize.h in order to read texture)

    Creating a window using GLFW

    Here is the Window Guide form the GLFW website, but here is the summary:

    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
            //
            // We can do things here
            //
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    Alright now that we have a window we can finally start using OpenGL

    Initializing GLEW and our first OpenGL call

    We simply have to call glewInit() and the OpenGL functions we want next!
    In our case, our first functions will be glClearColor()(so we select the color with which we will fill the window we clear) and glClear()
    GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT these are simply flags that tell OpenGL we want to clear the colors and the depth buffers of our frame buffer(don't worry about it, we will maybe explain this later!)
    Even more info in the docs, but here is the example:

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
        
        glewInit();
    
        glClearColor(1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    We should have a red window opened now!

    Alright, pretty simple so far, let's kick it up a notch and finally get to render a triangle on our screen!

    VAOs, VBOs and EBOs

    I will oversimplify this A ALOT, but what we are trying to do is basically send data to the GPU and tell it how to draw it...that's it really
    What are we sending? Well, mainly points that will be called from now on vertices that will have all sorts of details attached to them
    things like the position and color (for now).

    Ok, now we sort of know what we will send to the GPU now let's see how we will go about putting the data together and actually sending it!
    For this we will use something called Vertex Array Object (VAO) which will be used in order to link multiple buffers together, these buffers, for us
    are Vertex Buffer Objects(VBOs) and a Element Buffer Object(EBO) which I will explain in a second. Basically this is what we want!
    image

    Ok, ok, I will try to explain what we will use these for and then show you the code. We will basically put our vertices(our points) inside the VBO
    And we will put the order in which the vertices should be connected, in the EBO

    image

    Since we will be using triangle based models(meaning that the faces of the model will be arranged in triangles), we will group 3 points at a time!
    Alright, do you remember when I said we can have multiple things assigned to a vertex(one of the point we send)? In the above image we only sent a position, in the future we will send more stuff!

    Ok let's actually implement this and we will fill in the blanks on the go.

    struct Vertex {
        glm::vec3 position;
    };
    
    using Index = int;
    using BufferId = GLuint;
    
    struct VAO {
        BufferId id;
        BufferId vbo;
        BufferId ebo;
        int facesUsed;
        int verticesUsed;
    
        void PushVerticesIntoBuffer(const std::vector<Vertex>& vertices);
        void PushFacesIntoBuffer(const std::vector<Index>& indices);
    };
    
    class OpenGL {
    public:
        static VAO CreateVertexArray(int numberOfVertices, int numberOfFaces);
    };
    
    VAO OpenGL::CreateVertexArray(int numberOfVertices, int numberOfFaces) {
        VAO vao = {};
        glGenVertexArrays(1, &vao.id);
        glBindVertexArray(vao.id);
    
        glGenBuffers(1, &vao.vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vao.vbo);
        glBufferData(GL_ARRAY_BUFFER, numberOfVertices * sizeof(Vertex) , NULL, GL_DYNAMIC_DRAW);
    
        glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(Vertex), NULL);
        glEnableVertexAttribArray(0);
    
        glGenBuffers(1, &vao.ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vao.ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, numberOfFaces * 3 * sizeof(Index), NULL, GL_DYNAMIC_DRAW);
    
        return vao;
    }
    
    void VAO::PushVerticesIntoBuffer(const std::vector<Vertex>& vertices) {
        glNamedBufferSubData(vbo, sizeof(Vertex) * verticesUsed, sizeof(Vertex) * vertices.size(), (void*)(vertices.data()));
        verticesUsed += vertices.size();
    }
    
    void VAO::PushFacesIntoBuffer(const std::vector<Index>& indices) {
        int offset = sizeof(Index) * facesUsed * 3;
        glNamedBufferSubData(ebo, offset, indices.size() * sizeof(Index), (void*)(indices.data()));
        facesUsed += indices.size() / 3;
    }

    Not to go too much in detail, but here we are creating a VAO with its VBO and EBO, and creating some functions that can load data into those buffers. We will see how we use them shortly, but before we get to do that, we need to do some more setup.

    Shaders

    Shaders are programs that run on our GPU, we are mainly interested in 2 types of shaders, a Vertex Shader and a Fragment Shader. The Vertex Shader is a program we will write that will tell the GPU what to do with the vertices we send over(maybe offset them in a direction, think of water waves), and the Fragment Shader will decide how the faces should be colored.
    This is a obviously a simplified explanation of what these are.

    These shaders are written in a language similar to C!
    We will write these shaders, load them, compile them and display any errors, these shaders will be linked to a Program Id, basically whenever we want to use these shaders we will bind this Program Id, much like how we do for the VAO, VBO and EBO situation!

    Here is the code after adding support for shader compilation and linkage to our "shader program".

    struct Vertex {
        glm::vec3 position;
    };
    
    using Index = int;
    using GLId = GLuint;
    
    struct VAO {
        GLId id;
        GLId vbo;
        GLId ebo;
        int facesUsed;
        int verticesUsed;
    
        void PushVerticesIntoBuffer(const std::vector<Vertex>& vertices);
        void PushFacesIntoBuffer(const std::vector<Index>& indices);
    };
    
    struct ShaderProgram {
        GLId id;
        GLId vShaderId;
        GLId fShaderId;
    };
    
    class OpenGL {
    public:
        static VAO CreateVertexArray(int numberOfVertices, int numberOfFaces);
    
        static void RenderVAO(const VAO& vao, const ShaderProgram& program, GLenum mode);
    
        static ShaderProgram CreateShaderProgram(const std::string& vertexShader, const std::string& fragmentShader);
        static GLId CompileShader(const std::string& source, GLenum type);
    };
    
    void OpenGL::RenderVAO(const VAO& vao, const ShaderProgram& program, GLenum mode) {
        glBindVertexArray(vao.id);
    
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vao.ebo);
        
        glUseProgram(program.id);
    
        glDrawElements(mode, vao.facesUsed * 3, GL_UNSIGNED_INT, NULL);
    }
    
    GLId OpenGL::CompileShader(const std::string& source, GLenum type) {
        GLId shaderId = glCreateShader(type);
    
        const GLchar* src = source.c_str();
    
        glShaderSource(shaderId, 1, &src, NULL);
        glCompileShader(shaderId);
    
        GLint shaderStatus;
        glGetShaderiv(shaderId, GL_COMPILE_STATUS, &shaderStatus);
        if (shaderStatus != GL_TRUE) {
            GLsizei logLength = 0;
            GLchar message[1024];
            glGetShaderInfoLog(shaderId, 1024, &logLength, message);
            message[logLength] = 0;
            printf("%s\n", message);
            assert(false);
        }
    
        return shaderId;
    }
    
    ShaderProgram OpenGL::CreateShaderProgram(const std::string& vertexShader, const std::string& fragmentShader) {
        ShaderProgram result;
    
        result.vShaderId = CompileShader(vertexShader, GL_VERTEX_SHADER);
        result.fShaderId = CompileShader(fragmentShader, GL_FRAGMENT_SHADER);
    
        result.id = glCreateProgram();
    
        glAttachShader(result.id, result.vShaderId);
        glAttachShader(result.id, result.fShaderId);
        glLinkProgram(result.id);
    
        GLint programLinked;
        glGetProgramiv(result.id, GL_LINK_STATUS, &programLinked);
        if (programLinked != GL_TRUE) {
            GLsizei logLength = 0;
            GLchar message[1024];
            glGetProgramInfoLog(result.id, 1024, &logLength, message);
            std::cout << message << std::endl;
        }
    
        return result;
    }
    
    VAO OpenGL::CreateVertexArray(int numberOfVertices, int numberOfFaces) {
        VAO vao = {};
        glGenVertexArrays(1, &vao.id);
        glBindVertexArray(vao.id);
    
        glGenBuffers(1, &vao.vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vao.vbo);
        glBufferData(GL_ARRAY_BUFFER, numberOfVertices * sizeof(Vertex), NULL, GL_DYNAMIC_DRAW);
    
        glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(Vertex), NULL);
        glEnableVertexAttribArray(0);
    
        glGenBuffers(1, &vao.ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vao.ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, numberOfFaces * 3 * sizeof(Index), NULL, GL_DYNAMIC_DRAW);
    
        return vao;
    }
    
    void VAO::PushVerticesIntoBuffer(const std::vector<Vertex>& vertices) {
        glNamedBufferSubData(vbo, sizeof(Vertex) * verticesUsed, sizeof(Vertex) * vertices.size(), (void*)(vertices.data()));
        verticesUsed += vertices.size();
    }
    
    void VAO::PushFacesIntoBuffer(const std::vector<Index>& indices) {
        int offset = sizeof(Index) * facesUsed * 3;
        glNamedBufferSubData(ebo, offset, indices.size() * sizeof(Index), (void*)(indices.data()));
        facesUsed += indices.size() / 3;
    }

    Our first triangle, finally

    Ok, we are done with the boilerplate code, believe it or not, we won't have to change this code for a while.
    Now we need to define our points ((0.5, 0, 0), (0, 0.5, 0), (0, -0.5, 0)), identical to the example above.
    Define the order in which we will connect our points, not too hard since we only have 3, make sure you connect them in a clockwise order, just like in the image above. (0, 1, 2).

    And our shader programs, like I said they are very close to a normal C program.

    Vertex Shader

    #version 440 core
    
    layout(location = 0) in vec3 vertexPosition;
    
    void main(){
        gl_Position = vec4(vertexPosition, 1.0);
    }

    Fragment Shader

    #version 440 core
    
    out vec4 color;
    
    void main(){
        color = vec4(0, 1, 0, 1);
    }

    For now, we don't modify the vertex positions at all in the vertex shader, and in the fragment shader, we simply return the color green(r, g, b, a) so its (0, 1, 0, 1)

    We also added this function in the OpenGL class, the thing that allows us to render our VAO.

    void OpenGL::RenderVAO(const VAO& vao, const ShaderProgram& program, GLenum mode) {
        glBindVertexArray(vao.id);
    
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vao.ebo);
        
        glUseProgram(program.id);
    
        glDrawElements(mode, vao.facesUsed * 3, GL_UNSIGNED_INT, NULL);
    }

    And now, let's put it all together in our main function.

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    #include "opengl.hpp"
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        glewInit();
    
        VAO mesh = OpenGL::CreateVertexArray(3, 1);
    
        std::vector<Vertex> triangleVertices = {
            { {0.5, 0,  0}  },
            { {0,  0.5, 0}  },
            { {0, -0.5, 0}  }
        };
    
        std::vector<Index> triangleFaces = {
            0, 1, 2
        };
    
        mesh.PushVerticesIntoBuffer(triangleVertices);
        mesh.PushFacesIntoBuffer(triangleFaces);
    
        std::string vertexShader = R"(
            #version 440 core
    
            layout(location = 0) in vec3 vertexPosition;
    
            void main(){
                gl_Position = vec4(vertexPosition, 1.0);
            }
        )";
    
        std::string fragmentShader = R"(
            #version 440 core
    
            out vec4 color;
    
            void main(){
                color = vec4(0, 1, 0, 1);
            }
        )";
    
        ShaderProgram program = OpenGL::CreateShaderProgram(vertexShader, fragmentShader);
    
        glClearColor(1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            OpenGL::RenderVAO(mesh, program, GL_TRIANGLES);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    image

    This should give you a green triangle, congrats!

    Going 3D

    Ok, as you may have observed, our screen coordinates go from (1, 1) to (-1, -1), these are Screen Coordinates.
    image

    Our objects will be in world space, using world coordinates, like, a cube at (0, 1, 3), a person at(-1, 0, 1), and so on, at some point we will have to convert the points that make up these models into screen coordinates, this is how 3D projection works.

    For now I will leave some more links that hopefully can help you understand this process more!
    This one, and here.

    Ok now let's lay down the basics, what matrices we will need in order to transform our points in the way we want.
    There are 3 major matrices, that everyone should know at least what they do, later own we will break these matrices down and understand them more in depth.

    The Model matrix will hold transformation we make to our model, things like, move this more to the left, rotate by 20 degrees on the Y axis, scale by 1.2, and so on, basically when we move our object we will change this matrix!

    The View matrix will hold information about where the camera is in the scene and in which direction it points to, when we want to move the camera, we change this matrix.

    The Projection matrix will transform the points in screen space, such that the 3D(in our case) perspective works, this is doing the world to screen conversion we talked about!

    Just as a heads up, the order in which we apply these matrices matters!!!
    This is the normal order, first we transform our model (ModelMatrix * ourPoint), then we apply the View matrix (ViewMatrix * (ModelMatrix * ourPoint)) and at the very end we apply the world to screen transformation (ProjectionMatrix * (ViewMatrix * (ModelMatrix * ourPoint))).

    Ok, ok, for now let's do the most basic transformation, we don't move the model transformation, we don't move the camera transformation, we only apply the projection!

    Since we need to send matrices to the vertex shader, we have to use something called uniforms basically variables we can change at runtime in the shader.
    So we can add this to our ShaderProgram class.

    void ShaderProgram::SetUniformMatrix(const std::string& name, const glm::mat4& matrix) {
        glUseProgram(id);
        GLuint location = glGetUniformLocation(id, name.c_str());
        glUniformMatrix4fv(location, 1, false, &matrix[0][0]);
    }

    Here is the final code for having the perspective matrix applied:

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    #include "opengl.hpp"
    #include "glm/gtc/matrix_transform.hpp"
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        glewInit();
    
        VAO mesh = OpenGL::CreateVertexArray(3, 1);
    
        std::vector<Vertex> triangleVertices = {
            { {0.5, 0,  -1}  },
            { {0,  0.5, -1}  },
            { {0, -0.5, -1}  }
        };
    
        std::vector<Index> triangleFaces = {
            0, 1, 2
        };
    
        mesh.PushVerticesIntoBuffer(triangleVertices);
        mesh.PushFacesIntoBuffer(triangleFaces);
    
        std::string vertexShader = R"(
            #version 440 core
    
            layout(location = 0) in vec3 vertexPosition;
            
            uniform mat4 projectionMatrix;
    
            void main(){
                gl_Position = projectionMatrix * vec4(vertexPosition, 1.0);
            }
        )";
    
        std::string fragmentShader = R"(
            #version 440 core
    
            out vec4 color;
    
            void main(){
                color = vec4(0, 1, 0, 1);
            }
        )";
    
        ShaderProgram program = OpenGL::CreateShaderProgram(vertexShader, fragmentShader);
    
        glm::mat4 perspective = glm::perspective<float>(glm::radians(80.0), (float)width/(float)height, 0.01, 100000.0);
    
        program.SetUniformMatrix("projectionMatrix", perspective);
    
        glClearColor(1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            OpenGL::RenderVAO(mesh, program, GL_TRIANGLES);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    Now our triangle actually sits in a 3D space, the problem I have with this is that we had to move the triangle at -1 on the Z axis, and I usually like to have the camera by default looking at the positive Z direction, for this we have to include a camera into our scene, basically now we can add the view matrix.

    We will use the glm::lookAt matrix, and we will tell it that our eye position is at (0, 0, -1), the center is at (0, 0, 0) and up is (0, 1, 0) since I want the Y axis to be up!

    glm::mat4 camera = glm::lookAt(glm::vec3(0, 0, -1), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
    program.SetUniformMatrix("viewMatrix", camera);

    Our vertex shader looks like this now, only added one more matrix.

    #version 440 core
    
    layout(location = 0) in vec3 vertexPosition;
            
    uniform mat4 projectionMatrix;
    uniform mat4 viewMatrix;
    
    void main(){
        gl_Position = projectionMatrix * viewMatrix * vec4(vertexPosition, 1.0);
    }

    Alright, now let's render a cube, I took the vertices and faces from here, added a model matrix in order to move the cube around, and this is how the code looks like now!

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    #include "opengl.hpp"
    #include "glm/gtc/matrix_transform.hpp"
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        glewInit();
    
         std::vector<Vertex> cubeVertices = {
             // front
             {{ -1.0, -1.0,  1.0}},
             {{1.0, -1.0,  1.0}},
             {{1.0,  1.0,  1.0}},
             {{-1.0,  1.0,  1.0}},
             // back
             {{-1.0, -1.0, -1.0}},
             {{1.0, -1.0, -1.0}},
             {{1.0,  1.0, -1.0}},
             {{-1.0,  1.0, -1.0}}
        };
    
         std::vector<Index> cubeFaces = {
             // front
             0, 1, 2,
             2, 3, 0,
             // right
             1, 5, 6,
             6, 2, 1,
             // back
             7, 6, 5,
             5, 4, 7,
             // left
             4, 0, 3,
             3, 7, 4,
             // bottom
             4, 5, 1,
             1, 0, 4,
             // top
             3, 2, 6,
             6, 7, 3
         };
        VAO mesh = OpenGL::CreateVertexArray(cubeVertices.size(), cubeFaces.size());
    
        mesh.PushVerticesIntoBuffer(cubeVertices);
        mesh.PushFacesIntoBuffer(cubeFaces);
    
        std::string vertexShader = R"(
            #version 440 core
    
            layout(location = 0) in vec3 vertexPosition;
            
            uniform mat4 projectionMatrix;
            uniform mat4 viewMatrix;
            uniform mat4 modelMatrix;
    
            void main(){
                gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
            }
        )";
    
        std::string fragmentShader = R"(
            #version 440 core
    
            out vec4 color;
    
            void main(){
                color = vec4(0, 1, 0, 1);
            }
        )";
    
        ShaderProgram program = OpenGL::CreateShaderProgram(vertexShader, fragmentShader);
    
        glm::mat4 perspective = glm::perspective<float>(glm::radians(80.0), (float)width/(float)height, 0.01, 100000.0);
        glm::mat4 camera = glm::lookAt(glm::vec3(0, 0, -1), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
        glm::mat4 model = glm::mat4();
    
        model = glm::translate(model, { 0, 0, 2 });
    
        program.SetUniformMatrix("projectionMatrix", perspective);
        program.SetUniformMatrix("viewMatrix", camera);
    
        glClearColor(1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            model = glm::rotate(model, glm::radians(10.0f), glm::vec3(0, 1, 0));
            program.SetUniformMatrix("modelMatrix", model);
    
            OpenGL::RenderVAO(mesh, program, GL_TRIANGLES);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    We should now see a fast rotating cube!

    image

    Some changes to the vertex and fragment shader will give us a more colorful cube!
    image

    But uh oh, it seems the faces are not sorted properly, we can see through the model sometimes and back faces overlap the front ones, we need to enable depth testing so the GPU knows the order in which to render the faces!

    image

    This is the latest state of the code!

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    #include "opengl.hpp"
    #include "glm/gtc/matrix_transform.hpp"
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        glewInit();
    
         std::vector<Vertex> cubeVertices = {
             // front
             {{ -1.0, -1.0,  1.0}},
             {{1.0, -1.0,  1.0}},
             {{1.0,  1.0,  1.0}},
             {{-1.0,  1.0,  1.0}},
             // back
             {{-1.0, -1.0, -1.0}},
             {{1.0, -1.0, -1.0}},
             {{1.0,  1.0, -1.0}},
             {{-1.0,  1.0, -1.0}}
        };
    
         std::vector<Index> cubeFaces = {
             // front
             0, 1, 2,
             2, 3, 0,
             // right
             1, 5, 6,
             6, 2, 1,
             // back
             7, 6, 5,
             5, 4, 7,
             // left
             4, 0, 3,
             3, 7, 4,
             // bottom
             4, 5, 1,
             1, 0, 4,
             // top
             3, 2, 6,
             6, 7, 3
         };
        VAO mesh = OpenGL::CreateVertexArray(cubeVertices.size(), cubeFaces.size());
    
        mesh.PushVerticesIntoBuffer(cubeVertices);
        mesh.PushFacesIntoBuffer(cubeFaces);
    
        std::string vertexShader = R"(
            #version 440 core
    
            layout(location = 0) in vec3 vertexPosition;
            
            uniform mat4 projectionMatrix;
            uniform mat4 viewMatrix;
            uniform mat4 modelMatrix;
    
            out vec3 position;
    
            void main(){
                gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
                position = vertexPosition;
            }
        )";
    
        std::string fragmentShader = R"(
            #version 440 core
    
            out vec4 color;
    
            in vec3 position;
    
            void main(){
                color = vec4(position, 1);
            }
        )";
    
        ShaderProgram program = OpenGL::CreateShaderProgram(vertexShader, fragmentShader);
    
        glm::mat4 perspective = glm::perspective<float>(glm::radians(80.0), (float)width/(float)height, 0.01, 100000.0);
        glm::mat4 camera = glm::lookAt(glm::vec3(0, 0, -1), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
        glm::mat4 model = glm::mat4();
    
        model = glm::translate(model, { 0, 0, 2 });
    
        program.SetUniformMatrix("projectionMatrix", perspective);
        program.SetUniformMatrix("viewMatrix", camera);
    
        glEnable(GL_DEPTH_TEST);
        glClearColor(1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            model = glm::rotate(model, glm::radians(1.0f), glm::vec3(0, 1, 0));
            program.SetUniformMatrix("modelMatrix", model);
    
            OpenGL::RenderVAO(mesh, program, GL_TRIANGLES);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    Texturing

    We will send some data along with the position in order for us to know how to texture our faces!
    This data consists of a set of coordinates, mapping a point onto a texture, these coordinates are called UV Coordinates
    These range from 0 to 1!

    image

    Something like this, now lets add these UV coordinates as part of our vertex when we send it to the GPU!

    Here is a rotating quad with a texture on it.

    image

    We added this function in order to load textures onto the GPU

    GLId OpenGL::CreateTexture(const std::string& path) {
        GLId textureId;
    
        glGenTextures(1, &textureId);
        glBindTexture(GL_TEXTURE_2D, textureId);
    
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
        int width, height, nrChannels;
        unsigned char* data = stbi_load(path.c_str(), &width, &height, &nrChannels, 3);
        if (data) {
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
            stbi_image_free(data);
        } else {
            std::cout << "Failed to load texture" << std::endl;
        }
    
        return textureId;
    }

    And this is the code in the main.

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    #include "opengl.hpp"
    #include "glm/gtc/matrix_transform.hpp"
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        glewInit();
    
        std::vector<Vertex> quadVertices = {
            {{-1, 1, 1}, {0, 0}},
            {{1,  1, 1}, {1, 0}},
            {{1, -1, 1}, {1, 1}},
            {{-1, -1, 1}, {0, 1}},
        };
    
        std::vector<Index> quadFaces = {
            0, 1, 3,
            1, 2, 3
        };
    
        VAO mesh = OpenGL::CreateVertexArray(quadVertices.size(), quadFaces.size());
    
        mesh.PushVerticesIntoBuffer(quadVertices);
        mesh.PushFacesIntoBuffer(quadFaces);
    
        std::string vertexShader = R"(
            #version 440 core
    
            layout(location = 0) in vec3 vertexPosition;
            layout(location = 1) in vec2 vertexUV;
            
            uniform mat4 projectionMatrix;
            uniform mat4 viewMatrix;
            uniform mat4 modelMatrix;
    
            out vec2 fragUV;
    
            void main(){
                gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
                fragUV = vertexUV;
            }
        )";
    
        std::string fragmentShader = R"(
            #version 440 core
    
            out vec4 color;
    
            in vec2 fragUV;
    
            uniform sampler2D image;
    
            void main(){
                color = texture(image, fragUV);
            }
        )";
    
        ShaderProgram program = OpenGL::CreateShaderProgram(vertexShader, fragmentShader);
    
        glm::mat4 perspective = glm::perspective<float>(glm::radians(80.0), (float)width/(float)height, 0.01, 100000.0);
        glm::mat4 camera = glm::lookAt(glm::vec3(0, 0, -1), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
        glm::mat4 model = glm::mat4();
    
        model = glm::translate(model, { 0, 0, 2 });
    
        program.SetUniformMatrix("projectionMatrix", perspective);
        program.SetUniformMatrix("viewMatrix", camera);
    
        GLId woodTexture = OpenGL::CreateTexture("wood.png");
    
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, woodTexture);
    
        glEnable(GL_DEPTH_TEST);
        glClearColor(1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            model = glm::rotate(model, glm::radians(1.0f), glm::vec3(0, 1, 0));
            program.SetUniformMatrix("modelMatrix", model);
    
            OpenGL::RenderVAO(mesh, program, GL_TRIANGLES);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    (Notice that we made changes in the fragment shader and we added some GL calls for the active texture and what texture we want to bind!)

    Importing a 3D model

    Alright, now we can put together everything we know so far and import a 3D model using ASSIMP, now there is a good amount of code we need, but the good part is that, the code is pretty straight forward, we can have multiple models in a single file, so we will join these in a single buffer, both their vertices and their faces, what we need to keep in mind is that the faces for other models will need to be offset by how the amount of vertices we previously loaded because they all start their index from 0, but those vertices won't necessarily start from 0 in our buffer.

    For now this is our mesh loader header:

    #pragma once
    
    #include "opengl.hpp"
    
    #include <assimp/cimport.h>
    #include <assimp/scene.h>
    #include <assimp/postprocess.h>
    
    #include "utility.hpp"
    
    class Mesh {
    public:
    	VAO vao;
    	GLId diffuseTexture;
    
    	Mesh() :vao(), diffuseTexture(-1) {}
    };
    
    struct Loader {
    	static Mesh LoadMesh(const std::string& path);
    	static void PushAllVerticesAndFacesIntoMesh(const aiScene* scene, Mesh* mesh, const std::vector<int>& vertexOffsets);
    	static void PopulateVertexOffsetsPerMesh(const aiScene* scene, std::vector<int>* offsets);
    	static int ComputeTotalVertices(const aiScene* scene);
    	static int ComputeTotalFaces(const aiScene* scene);
    };

    And this is the implementation!(for now)

    #include "model_importer.hpp"
    
    #include "opengl.hpp"
    
    #include <assimp/cimport.h>
    #include <assimp/scene.h>
    #include <assimp/postprocess.h>
    
    #include "utility.hpp"
    
    void Loader::PopulateVertexOffsetsPerMesh(const aiScene* scene, std::vector<int>* offsets) {
    	int offset = 0;
    
    	offsets->push_back(offset);
    	for (int i = 0; i < scene->mNumMeshes; ++i) {
    		offset += scene->mMeshes[i]->mNumVertices;
    
    		offsets->push_back(offset);
    	}
    }
    
    int Loader::ComputeTotalVertices(const aiScene* scene) {
    	int result = 0;
    	for (int i = 0; i < scene->mNumMeshes; ++i) {
    		result += scene->mMeshes[i]->mNumVertices;
    	}
    	return result;
    }
    
    int Loader::ComputeTotalFaces(const aiScene* scene) {
    	int result = 0;
    	for (int i = 0; i < scene->mNumMeshes; ++i) {
    		result += scene->mMeshes[i]->mNumFaces;
    	}
    	return result;
    }
    
    void Loader::PushAllVerticesAndFacesIntoMesh(const aiScene* scene, Mesh* mesh, const std::vector<int>& vertexOffsets) {
    	std::vector<Vertex> vertices;
    	std::vector<Index> faces;
    
    	for (int meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex) {
    		aiMesh* sceneMesh = scene->mMeshes[meshIndex];
    
    		for (int v = 0; v < sceneMesh->mNumVertices; ++v) {
    			aiVector3D position = sceneMesh->mVertices[v];
    			aiVector3D uv = {};
    			if (sceneMesh->mTextureCoords[0]) {
    				uv = sceneMesh->mTextureCoords[0][v];
    			}
    
    			vertices.push_back(Vertex(Utility::ConvertAIv3ToGLMv3(position), Utility::ConvertAIv3ToGLMv3(uv)));
    		}
    
    		for (int f = 0; f < sceneMesh->mNumFaces; ++f) {
    			aiFace face = sceneMesh->mFaces[f];
    			assert(face.mNumIndices == 3);
    			faces.push_back(face.mIndices[0] + vertexOffsets[meshIndex]);
    			faces.push_back(face.mIndices[1] + vertexOffsets[meshIndex]);
    			faces.push_back(face.mIndices[2] + vertexOffsets[meshIndex]);
    		}
    	}
    
    	mesh->vao.PushVerticesIntoBuffer(vertices);
    	mesh->vao.PushFacesIntoBuffer(faces);
    }
    
    Mesh Loader::LoadMesh(const std::string& path) {
    	const aiScene* scene = aiImportFile(path.c_str(), aiProcess_Triangulate | aiProcess_FlipUVs);
    
    	std::vector<int> vertexOffsets;
    	Mesh result;
    
    	result.vao = OpenGL::CreateVertexArray(ComputeTotalVertices(scene), ComputeTotalFaces(scene));
    	PopulateVertexOffsetsPerMesh(scene, &vertexOffsets);
    
    	PushAllVerticesAndFacesIntoMesh(scene, &result, vertexOffsets);
    
    	return result;
    }

    I got this model this model from sketchfab.com.
    Download the obj file, unzip it and copy the obj file somewhere where you can find it from our executable.

    This is how we load it

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    #include "opengl.hpp"
    #include "model_importer.hpp"
    #include "glm/gtc/matrix_transform.hpp"
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        glewInit();
    
        Mesh mesh = Loader::LoadMesh("cans_redbull.obj");
    
        std::string vertexShader = R"(
            #version 440 core
    
            layout(location = 0) in vec3 vertexPosition;
            layout(location = 1) in vec3 vertexUV;
            
            uniform mat4 projectionMatrix;
            uniform mat4 viewMatrix;
            uniform mat4 modelMatrix;
    
            out vec3 fragUV;
    
            void main(){
                gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
                fragUV = vertexUV;
            }
        )";
    
        std::string fragmentShader = R"(
            #version 440 core
    
            out vec4 color;
    
            in vec3 fragUV;
    
            uniform sampler2D image;
    
            void main(){
                color = texture(image, fragUV.xy);
            }
        )";
    
        ShaderProgram program = OpenGL::CreateShaderProgram(vertexShader, fragmentShader);
    
        glm::mat4 perspective = glm::perspective<float>(glm::radians(80.0), (float)width/(float)height, 0.01, 100000.0);
        glm::mat4 camera = glm::lookAt(glm::vec3(0, 0, -1), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
        glm::mat4 model = glm::mat4();
    
        model = glm::translate(model, { 0, 0, 0 });
    
        program.SetUniformMatrix("projectionMatrix", perspective);
        program.SetUniformMatrix("viewMatrix", camera);
    
        GLId woodTexture = OpenGL::CreateTexture("wood.png");
    
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, woodTexture);
    
        glEnable(GL_DEPTH_TEST);
        glClearColor(1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            model = glm::rotate(model, glm::radians(1.0f), glm::vec3(0, 1, 0));
            program.SetUniformMatrix("modelMatrix", model);
    
            OpenGL::RenderVAO(mesh.vao, program, GL_TRIANGLES);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    image Oops, well, we forgot to load the textures that come with the model.

    Well, anyway, aperantly the obj didn't come with the material file for the red bulls cans above, so I downloaded a new model.
    From here, chose the fbx one.

    And made a few changes so I can have multiple meshes with multiple textures and everything still show up correctly, basically I created a opengl 2D texture array of size 1024x1024 each texture, and the array size is the amount of materials for now, and each mech will have one more component in their UVs which is the Z coordinate will be used to index in this array their texture.

    I changed this, so it takes 3 instead of 2 components, and changed from a glm::vec2 to a glm::vec3 in the Vertex struct.

    glVertexAttribPointer(1, 3, GL_FLOAT, false, sizeof(Vertex), (void*)offset);
    glEnableVertexAttribArray(1);

    Here are the new functions so far!

    void Loader::LoadMaterials(const aiScene* scene, Mesh* mesh) {
    	mesh->textureArrayId = OpenGL::CreateTextureArray(1024, 1024, scene->mNumMaterials);
    
    	for (int i = 0; i < scene->mNumMaterials; ++i) {
    		aiString path;
    
    		aiMaterial* material = scene->mMaterials[i];
    		aiString texture_file;
    		material->Get(AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0), texture_file);
    		if (auto texture = scene->GetEmbeddedTexture(texture_file.C_Str())) {
    			OpenGL::TextureArrayInsert(mesh->textureArrayId, (unsigned char*)texture->pcData, texture->mWidth, texture->mHeight, i, texture->mHeight == 0);
    		}
    		else {
    			OpenGL::TextureArrayInsert(mesh->textureArrayId, texture_file.C_Str(), i);
    		}
    	}
    }

    And in the OpenGL class

    GLId OpenGL::CreateTextureArray(int width, int height, int size) {
        GLId result;
        glGenTextures(1, &result);
        glBindTexture(GL_TEXTURE_2D_ARRAY, result);
    
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, width, height, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
        return result;
    }
    
    void OpenGL::TextureArrayInsert(GLId id, const std::string& path, int slot) {
        int width, height, nrChannels;
        unsigned char* data = stbi_load(path.c_str(), &width, &height, &nrChannels, 4);
        TextureArrayInsert(id, data, width, height, slot, false);
        stbi_image_free(data);
    }
    
    void OpenGL::TextureArrayInsert(GLId id, unsigned char* data, int width, int height, int slot, bool decodeFromMemory) {
        glBindTexture(GL_TEXTURE_2D_ARRAY, id);
        if (decodeFromMemory) {
            int w, h, nrChannels;
            unsigned char* decodedData = stbi_load_from_memory(data, width, &w, &h, &nrChannels, 4);
    
            if (decodedData) {
                unsigned char* resizedImageData = nullptr;
                if (w > 1024 && h > 1024) {
                    resizedImageData = new unsigned char[1024 * 1024 * 4];
    
                    stbir_resize_uint8(decodedData, w, h, 0,
                        resizedImageData, 1024, 1024, 0, 4);
    
                    if (resizedImageData) {
                        stbi_image_free(decodedData);
                        w = h = 1024;
                        decodedData = resizedImageData;
                    }
                }
                std::cout << glGetError() << std::endl;
                glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, slot, w, h, 1, GL_RGBA, GL_UNSIGNED_BYTE, decodedData);
                std::cout << glGetError() << std::endl;
                stbi_image_free(decodedData);
            }
            else {
                std::cout << "Failed to decode image from memory!" << std::endl;
            }
        }
        else {
            glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, slot, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, data);
        }
    }

    Here is the new fragment shader!

    #version 440 core
    
    out vec4 color;
    
    in vec3 fragUV;
    
    uniform sampler2DArray image;
    
    void main(){
        color = texture(image, fragUV);
    }

    This is our main now!

    #define GLEW_STATIC
    #include "GL/glew.h"
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    
    #include "opengl.hpp"
    #include "model_importer.hpp"
    #include "glm/gtc/matrix_transform.hpp"
    
    int main() {
        glfwInit();
        int width = 1280;
        int height = 768;
        GLFWwindow* window = glfwCreateWindow(width, height, "Our Window Title", NULL, NULL);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(1);
    
        glewInit();
    
    
        std::string vertexShader = R"(
            #version 440 core
    
            layout(location = 0) in vec3 vertexPosition;
            layout(location = 1) in vec3 vertexUV;
            
            uniform mat4 projectionMatrix;
            uniform mat4 viewMatrix;
            uniform mat4 modelMatrix;
    
            out vec3 fragUV;
    
            void main(){
                gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
                fragUV = vertexUV;
            }
        )";
    
        std::string fragmentShader = R"(
            #version 440 core
    
            out vec4 color;
    
            in vec3 fragUV;
    
            uniform sampler2DArray image;
    
            void main(){
                color = texture(image, fragUV);
            }
        )";
    
        Mesh mesh = Loader::LoadMesh("GamePadExported.fbx");
    
        ShaderProgram program = OpenGL::CreateShaderProgram(vertexShader, fragmentShader);
    
        glm::mat4 perspective = glm::perspective<float>(glm::radians(80.0), (float)width/(float)height, 0.01, 100000.0);
        glm::mat4 camera = glm::lookAt(glm::vec3(0, 0, -1), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
        glm::mat4 model = glm::mat4();
    
        model = glm::translate(model, { 0, 0, 10 });
        model = glm::scale(model, { 1, 1, 1 });
    
        program.SetUniformMatrix("projectionMatrix", perspective);
        program.SetUniformMatrix("viewMatrix", camera);
    
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D_ARRAY, mesh.textureArrayId);
        std::cout << glGetError() << std::endl;
    
        glEnable(GL_DEPTH_TEST);
        glClearColor(0.1, 0, 0, 1);
        while (!glfwWindowShouldClose(window)) {
            glfwSwapBuffers(window);
            glfwPollEvents();
    
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            model = glm::rotate(model, glm::radians(1.0f), glm::vec3(1, 1, 1));
            program.SetUniformMatrix("modelMatrix", model);
    
            OpenGL::RenderVAO(mesh.vao, program, GL_TRIANGLES);
        }
    
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    Our result!
    image