Renderer

Draw Meshes

In order to draw different objects we need to tell OpenGL what is the geometry of the object. This is done with what is called the vertex data. The vertex data defines the geometry of an object and it will be transformed into the actual object in the Graphics Pipeline, during the rasterization process.

OpenGL does not care how we structure our data as long as we give it a pointer to the array of vertices data. However, we like to make data a little bit more organized and flexible for re-usability purposes. For this we will create a mesh type that will contain the information related to the object geometry and, later on the material.

Mesh

The Mesh is a term broadly used in 3d programs to store information about the geometry of an object, it’s material, and sometimes the transform (position, rotation, scale). We want to start first by adding the information about the geometry.

Here we have 3 different types of meshes, a triangle, a quad (rectangle), and a cube. All of them contain the same array of floats but with different number of vertices.

struct triangle_mesh {
    float Vertices[9];
};

struct quad_mesh {
    float Vertices[12];
    GLuint Indices[6];
};

struct cube_mesh {
    float Vertices[288];
    GLuint Indices[36];
};

Notice how the quad_mesh and the cube_mesh have also another property called Indices. With this property we can instruct OpenGL how to draw the object reusing the existing vertices. This makes drawing a bit more efficient.

This looks good already but what if we wanted to draw more complex objects? Like a 3D Model that we build with blender? Do we have to add a struct for every new geometry added to the scene?

The answer is no. We can create a more generic mesh type that will be used for any type of object.

struct vertex {
    glm::vec3 Position;
    glm::vec3 Normal;
    glm::vec2 TexCoords;
};

struct mesh {
    std::vector<vertex> Vertices;
    std::vector<unsigned int> Indices;
};

This mesh type now uses the std::vector to store a dynamic number of vertices and indices. We have also created a new type called vertex this is just to define more granular the data of each vertex. The vertex at a minimum should contain the Position information, but we can add lots more there that can later be used in the vertex shader or fragment shader.

void Mesh_Create(mesh *Mesh, std::vector<vertex> Vertices,
                 std::vector<GLuint> Indices) {
    Mesh->Vertices = Vertices;
    Mesh->Indices = Indices;

    Mesh_Setup(Mesh);
}

We can now define what a quad or a cube is in terms of these new types.

void Mesh_CreateQuad(mesh *Mesh) {
    std::vector<vertex> Vertices = {
        {glm::vec3(-0.5f, -0.5f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(0.0f, 0.0f)},
        {glm::vec3(0.5f, -0.5f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(0.5f, 0.5f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(-0.5f, 0.5f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(0.0f, 1.0f)},
    };

    std::vector<GLuint> Indices = {0, 1, 2, 2, 3, 0};

    Mesh_Create(Mesh, Vertices, Indices);
}

void Mesh_CreateCube(mesh *Mesh) {
    std::vector<vertex> Vertices = {
        // Front Face
        {glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec2(0.0f, 0.0f)},
        {glm::vec3(0.5f, -0.5f, -0.5f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(-0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec2(0.0f, 0.0f)},

        // Back Face
        {glm::vec3(-0.5f, -0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(0.0f, 0.0f)},
        {glm::vec3(0.5f, -0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(-0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(-0.5f, -0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec2(0.0f, 0.0f)},

        // Left Face
        {glm::vec3(-0.5f, 0.5f, 0.5f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(-0.5f, 0.5f, -0.5f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(-0.5f, -0.5f, 0.5f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec2(0.0f, 0.0f)},
        {glm::vec3(-0.5f, 0.5f, 0.5f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec2(1.0f, 0.0f)},

        // Right Face
        {glm::vec3(0.5f, 0.5f, 0.5f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(0.5f, 0.5f, -0.5f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(0.5f, -0.5f, -0.5f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(0.5f, -0.5f, -0.5f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(0.5f, -0.5f, 0.5f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec2(0.0f, 0.0f)},
        {glm::vec3(0.5f, 0.5f, 0.5f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec2(1.0f, 0.0f)},

        // Bottom Face
        {glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(0.5f, -0.5f, -0.5f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(0.5f, -0.5f, 0.5f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(0.5f, -0.5f, 0.5f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(-0.5f, -0.5f, 0.5f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec2(0.0f, 0.0f)},
        {glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec2(0.0f, 1.0f)},

        // Top Face
        {glm::vec3(-0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec2(0.0f, 1.0f)},
        {glm::vec3(0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec2(1.0f, 1.0f)},
        {glm::vec3(0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec2(1.0f, 0.0f)},
        {glm::vec3(-0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec2(0.0f, 0.0f)},
        {glm::vec3(-0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec2(0.0f, 1.0f)},
    };

    std::vector<GLuint> Indices = {
        // Front face
        0, 2, 1, 2, 5, 4,
        // Back face
        6, 7, 8, 8, 10, 11,
        // Left face
        12, 13, 14, 14, 16, 17,
        // Right face
        18, 20, 19, 20, 23, 22,
        // Bottom face
        24, 25, 26, 26, 28, 29,
        // Top face
        30, 32, 31, 32, 35, 34,
    };

    Mesh_Create(Mesh, Vertices, Indices);
}

That is quite some data and this are just the simple objects.

The last step to make the mesh ready for drawing is the Mesh_Setup function. We need to setup the OpenGL buffers data for each of this objects. For this we need to set up a VAO (Vertex Array Object), a VBO (Vertex Buffer Object), and an EBO (Element Buffer Object). With the VBO bound, we need to tell OpenGL the arrangement of the vertex data and the indices data. We have to also tell OpenGL where the Position, Normal and TexCoords of the object are in a vertex. This allows us to access them later on in the vertex shader.

First, we’ll expand the mesh type with the OpenGL buffer pointers:

struct mesh {
    std::vector<vertex> Vertices;
    std::vector<unsigned int> Indices;

    // OpenGL buffers
    GLuint VAO;
    GLuint VBO;
    GLuint EBO;
};

Now, this is the setup that we need to do for each of the object types:

void Mesh_Setup(mesh *Mesh) {
    // create buffers/arrays
    glGenVertexArrays(1, &Mesh->VAO);
    glGenBuffers(1, &Mesh->VBO);
    glGenBuffers(1, &Mesh->EBO);

    glBindVertexArray(Mesh->VAO);
    // load data into vertex buffers
    glBindBuffer(GL_ARRAY_BUFFER, Mesh->VBO);
    // A great thing about structs is that their memory layout is sequential for
    // all its items. The effect is that we can simply pass a pointer to the
    // struct and it translates perfectly to a glm::vec3/2 array which again
    // translates to 3/2 floats which translates to a byte array.
    glBufferData(GL_ARRAY_BUFFER, Mesh->Vertices.size() * sizeof(vertex),
                 &Mesh->Vertices[0], GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, Mesh->EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                 Mesh->Indices.size() * sizeof(unsigned int), &Mesh->Indices[0],
                 GL_STATIC_DRAW);

    // set the vertex attribute pointers
    // vertex Positions
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (void *)0);
    // vertex normals
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(vertex),
                          (void *)offsetof(vertex, Normal));
    // vertex texture coords
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(vertex),
                          (void *)offsetof(vertex, TexCoords));
    glBindVertexArray(0);
}

Drawing Meshes

Finally, is time to draw meshes. For this is just need to make sure to activate the correct shader program and bind the mesh VAO. Then we call the glDrawElements function passing the about of indices that we want to draw.

void Mesh_Draw(GLuint ShaderID, mesh *Mesh) {
    glUseProgram(ShaderID);

    glBindVertexArray(Mesh->VAO);
    glDrawElements(GL_TRIANGLES,
                   static_cast<unsigned int>(Mesh->Indices.size()),
                   GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);
}

As an example, we can create some entities with their corresponding meshes and add them to the scene to be drawn.

entity Entity = {
    .Type = entity_type::CubeMesh,
    .Position = glm::vec3(2.0f, 0.0f, 0.0f),
    .Scale = glm::vec3(0.0f),
    .Rotation = glm::vec4(0.0f, 1.0f, 0.3f, 0.5f),
    .Color = glm::vec4(1.0f, 0.5f, 0.31f, 1.0f),
};

Scene_AddEntity(Scene, Entity);

...
// In the renderer.cpp

void Renderer_DrawScene(const renderer &Renderer, const scene &Scene,
                        const context &Context) {
    for (entity Entity : Scene.Entities) {
        switch (Entity.Type) {
        case entity_type::Cube:
            Renderer_DrawCube(
                Renderer,
                Entity.Position,
                Entity.Rotation,
                Entity.Color,
                Entity.Mesh,
            );
            break;
        case entity_type::Triangle:
            ...
            break;
        case entity_type::Quad:
            ...
            break;
        }
    }
}

void Renderer_DrawCube(const renderer &Renderer,
                       glm::vec<3, float> Position,
                       glm::vec<4, float> Rotation,
                       glm::vec<4, float> Color, mesh CubeMesh) {
    glUseProgram(Renderer.ShaderProgram.ID);

    glm::vec3 CubeColor = glm::vec3(Color[0], Color[1], Color[2]);
    glUniform3fv(Renderer.ShaderProgram.Uniforms.EntityColorUniformLoc, 1,
                 glm::value_ptr(CubeColor));

    glm::mat4 Model = glm::mat4(1.0f);
    Model = glm::translate(Model, Position);
    Model = glm::scale(Model, glm::vec3(1.0f));

    glm::vec3 RotationVec = glm::vec3(Rotation[1], Rotation[2], Rotation[3]);
    Model = glm::rotate(Model, glm::radians(Rotation[0]), RotationVec);
    glUniformMatrix4fv(Renderer.ShaderProgram.Uniforms.ModelUniformLoc, 1,
                       GL_FALSE, glm::value_ptr(Model));

    Mesh_Draw(Renderer.ShaderProgram.ID, &CubeMesh);
}

In the previous snippet we’re adding a new cube mesh to the scene and drawing all the entities that belong to the scene. When drawing the cube mesh, we’re also setting some of the uniforms that are needed to perform the vertex transformations, and finally we’re calling the Mesh_Draw function to draw the geometry.

Before we move on to drawing more complex scenes and adding cool lighting effects, we want to add a nice flying camera that allows us to move around the scene using the mouse and some keys.

Squared Wave SVG