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.