GraphicsBlast
The Basics | C++ | Windows | Command Line

Lesson 12: Entities

Entities!
Entities: We efficiently re-use imported models to begin building a scene!

In our previous lesson, we looked at loading Wavefront OBJ models into our program so we didn't have to hard-code all our vertex data into our source code. We implemented these models as a class, which took the model's filename as an input, read and processed it's contents, and then made it ready for rendering.

If we look at real-world scenarios though, we often want to have many objects in our world with the same underlying 3D model.

Of course a simple way to achieve this would be to load our model several times and position each these at a different location.

It would be a fairly inefficient solution though - for each object we would duplicate the entire model in GPU memory, and when our program starts have to wait for the file to be read multiple times.

A much better solution would be to load each model into our world once and only once, preventing duplication of work and memory. Then we can have each object in our world simply reference the model which it should draw with, but otherwise have it's own position and rotation data.

So to create three instances of our crate, we would create three "entity" objects, each with its own model matrix to position and orient it, and for the drawing hold a pointer to a model object. We can also extend this to textures too, and have each texture loaded just once, and have each entity just keep a reference to which texture it should be drawn with.

That sounds much more efficient!

Entity Class

So let's begin constructing an entity class, which will represent an object in our virtual world which we will render.

We'll begin by writing the class's definition to the header file entity.h:

+ 1.
#pragma once
+ 2.
+ 3.
#include "model.h"
+ 4.
#include "texture.h"
+ 5.
+ 6.
#include <GL/glew.h>
+ 7.
#include <glm/glm.hpp>
+ 8.
#include <glm/gtc/type_ptr.hpp>
+ 9.
#include <glm/ext/matrix_transform.hpp>

We as always start with a Pragma Once to make sure we can safely include the header file in multiple parts of our code without redefinition problems.

As our entity will be keeping a pointer to it's model and texture, we include model.h and texture.h.

We then include the GLEW and GLM library header files, which we'll need for using OpenGL functions and types, and for handling the entity's model matrix. From GLM, we load the core library, as well as type_ptr for accessing the raw type's data, and matrix_transform which we'll use to build the model matrix.

We can now construct the class definition for our entity:

+ 11.
class Entity
+ 12.
{
+ 13.
    public:
+ 14.
        Entity();
+ 15.
+ 16.
        void setModel(Model* newModel);
+ 17.
        void setTexture(Texture* newTexture);
+ 18.
+ 19.
        void setPosition(float newX, float newY, float newZ);
+ 20.
        void setOrientation(float newRX, float newRY, float newRZ);
+ 21.
+ 22.
        void draw();
+ 23.
+ 24.
    private:
+ 25.
        Model* model;
+ 26.
        Texture* texture;
+ 27.
+ 28.
        float x, y, z;
+ 29.
        float rx, ry, rz;
+ 30.
+ 31.
        glm::mat4 modelMatrix;
+ 32.
        void updateModelMatrix();
+ 33.
};

The class has a constructor to make sure the variables are initialised to safe starting values.

Then we declare a pair of functions for setting the entity's model and texture which it will be drawn with. These functions expect a pointer to the relevant model and texture, ensuring that the raw data won't be copied in.

After that we have a pair of functions for setting the entity's position and orientation. Setting the position requires a new 3D coordinate for the model, while setting the entity's orientation expects to be passed the new roll, pitch and yaw values in degrees. Note that the roll, pitch and yaw, correspond to a rotation in the X, Y, and Z axes respectively, known as the Tait-Bryan angles which we covered previously.

The final public function we'll set up for the entity is for drawing, which will take care of making sure the correct model, texture, and model matrix is set up and bound before making a draw call to OpenGL.

The class has a few member variables for storing these various values. I've also included a variable for the model matrix. While we could create this from the position and orientation every time we draw, it will only change when either setPosition or setOrientation have been called. Therefore when these functions are called, we can calculate and store the new model matrix, and reuse it for each subsequent draw call, saving us from recalculating it each frame.

For convenience, the class has an internal function called updateModelMatrix which will do the actual updating of the model matrix.

Defining our entities

Let's fill in those functions we just declared in a new file called entity.cpp:

+ 1.
#include "entity.h"
+ 2.
+ 3.
Entity::Entity()
+ 4.
{
+ 5.
    model = NULL;
+ 6.
    texture = NULL;
+ 7.
+ 8.
    x = 0;
+ 9.
    y = 0;
+ 10.
    z = 0;
+ 11.
    rx = 0;
+ 12.
    ry = 0;
+ 13.
    rz = 0;
+ 14.
+ 15.
    updateModelMatrix();
+ 16.
}

We start by including the header file for the class, entity.h, and then defining the constructor.

In this case, the constructor sets the position and orientation variables to safe starting values, and makes sure our pointers are set to NULL. Pointers are undefined by default, so by explicitly initialising them to NULL, we can easily test if they are non-NULL later to ensure the object has been set up properly before attempting to draw with them.

The constructor finishes up by making a call to updateModelMatrix(), our function to generate a model matrix. As we have initialised everything to zero, this will simply generate an identity matrix, but by calling it we ensure that the model matrix is initialised and always set to a valid state.

We can then fill in the functions for setting the entity's model and texture:

15.
    updateModelMatrix();
16.
}
17.
+ 18.
void Entity::setModel(Model* newModel)
+ 19.
{
+ 20.
    model = newModel;
+ 21.
}
+ 22.
+ 23.
void Entity::setTexture(Texture* newTexture)
+ 24.
{
+ 25.
    texture = newTexture;
+ 26.
}

These setter functions simply take a pointer to a model or texture, and store them within the object for drawing with later.

Next up are the entity's position and orientation setter functions:

25.
    texture = newTexture;
26.
}
27.
+ 28.
void Entity::setPosition(float newX, float newY, float newZ)
+ 29.
{
+ 30.
    x = newX;
+ 31.
    y = newY;
+ 32.
    z = newZ;
+ 33.
    updateModelMatrix();
+ 34.
}
+ 35.
+ 36.
void Entity::setOrientation(float newRX, float newRY, float newRZ)
+ 37.
{
+ 38.
    rx = newRX;
+ 39.
    ry = newRY;
+ 40.
    rz = newRZ;
+ 41.
    updateModelMatrix();
+ 42.
}

Fairly straight-forward again, we take the new coordinates or rotation values and store them. Each function finishes with a call to updateModelMatrix() to update the model matrix taking into account these new values.

Let's have a look at that function next:

41.
    updateModelMatrix();
42.
}
43.
+ 44.
void Entity::updateModelMatrix()
+ 45.
{
+ 46.
    glm::mat4 t = glm::mat4(1.0f);
+ 47.
    t = glm::translate(t, glm::vec3(x, y, z));
+ 48.
+ 49.
    glm::mat4 r = glm::mat4(1.0f);
+ 50.
+ 51.
    float rxRadians = (rx * 3.1415) / 180;
+ 52.
    float ryRadians = (ry * 3.1415) / 180;
+ 53.
    float rzRadians = (rz * 3.1415) / 180;
+ 54.
+ 55.
    r = glm::rotate(r, rzRadians, glm::vec3(0.0, 0.0, 1.0));
+ 56.
    r = glm::rotate(r, ryRadians, glm::vec3(0.0, 1.0, 0.0));
+ 57.
    r = glm::rotate(r, rxRadians, glm::vec3(1.0, 0.0, 0.0));
+ 58.
+ 59.
    modelMatrix = t * r;
+ 60.
}

To construct the model matrix, we'll first need to build both a translation and a rotation matrix.

Constructing the translation matrix is easy. We make a call to glm::mat4(1.0f) which will give us an empty 4x4 matrix with 1 on the diagonal, a new identity matrix.

Then we just translate it into position with a call to glm::translate, passing in the matrix to be translated, and a translation vector of type glm::vec3.

Creating the rotation matrix takes a few more steps. We again start by creating a new 4x4 identity matrix.

First, as all our rotations are in degrees, we need to convert them to radians which GLM expects. So we multiply these variables by π and then divide them by 180.

We then need to sequentially apply each rotation to the matrix. Remember, rotations in 3D are not commutative so the order we apply them is important. For Tait-Bryan angles, the rotation must first by applied to the Z-axis, then Y, then X. For this reason you'll often see Tait-Bryan angles described as "ZYX" rotations.

We can do this by using the function glm::rotate, which first expects to be passed an input matrix that the rotation will be applied to. This is then followed by the angle to rotate the matrix by in radians. The final parameter is then the axis in which the rotation should be applied. This is passed again as a glm::vec3 corresponding to the X, Y, and Z axes. So for the first rotation we apply, rzRadians, the rotation is applied around the z-axis.

Multiplying the translation and rotation matrices together then gives us a transformation matrix which sequentially applies the rotations and then the translation. Remember, mathematically matrix multiplication goes from right to left. In effect, this new matrix will apply each of these transformations to any vertices, rotating them and then positioning them in our world - our model matrix!

With our model matrix calculated, we can move on to our final class function to implemented: performing the entity's drawing!

59.
    modelMatrix = t * r;
60.
}
61.
+ 62.
void Entity::draw()
+ 63.
{
+ 64.
    if(model == NULL || texture == NULL)
+ 65.
        return;
+ 66.
+ 67.
    glActiveTexture(GL_TEXTURE0);
+ 68.
    texture->bind();
+ 69.
+ 70.
    model->bind();
+ 71.
+ 72.
    glUniformMatrix4fv(2, 1, GL_FALSE, glm::value_ptr(modelMatrix));
+ 73.
    glDrawElements(GL_TRIANGLES, model->getIndexCount(), GL_UNSIGNED_INT, 0);
+ 74.
+ 75.
    model->unbind();
+ 76.
    texture->unbind();
+ 77.
}

Our draw function starts by making sure that we actually have a valid model and texture with which to draw before doing anything. If these have not been set, the function simply returns, doing nothing.

If we have a model and texture, then we set the current texture unit to 0 and bind the texture associated with the entity. Likewise we bind the entity's model too.

We then make a call to glUniformMatrix4fv exactly as we done previously when drawing to set the model matrix, ensuring the model will be positioned correctly in our world. We then call glDrawElements to actually perform the drawing, using the model's index count for the number of indices to draw.

That's essentially it! We just finish up the function by making sure that the model and texture are then unbound before returning.

Updating our Makefile

Let's not forget that as we've created a new source file, we need to tell the compiler about it when building our program:

1.
CC = g++
2.
+ 3.
OBJS = main.cpp shader.cpp texture.cpp model.cpp entity.cpp
4.
5.
INCLUDE_DIRS = -IC:\SDL3\include -IC:\SDL3_image\include -IC:\glm -IC:\glew\include

We append the new source code file, entity.cpp, to our OBJS variable containing all files that need to be compiled when building. Don't forget, we only need to put the source code files here, not the header files!

Using Our Entities

With our new Entity class ready to go, let's create a few entities to go in our world. To start off with, we need to include our new header file into our main.cpp:

1.
#include "shader.h"
2.
#include "texture.h"
3.
#include "model.h"
+ 4.
#include "entity.h"

With the class and it's functions now available to our main.cpp, we can create a few entities for our world:

36.
Shader mainShader;
37.
Texture crateTexture;
38.
Model crateModel;
+ 39.
Entity crate1, crate2, crate3;
40.
41.
bool init()
42.
{

During initialisation, we can then set the properties of these entities:

126.
    crateModel.setFilename("resources/crate/crate.obj");
127.
    if(!crateModel.loadOBJModel())
128.
    {
129.
        printf("Unable to load model: %s\n", crateModel.getError().c_str());
130.
        return false;
131.
    }
132.
+ 133.
    crate1.setModel(&crateModel);
+ 134.
    crate1.setTexture(&crateTexture);
+ 135.
    crate1.setPosition(6, 0.46, 0);
+ 136.
    crate1.setOrientation(0, 0, 5);
137.
138.
    ...

After our models and textures have loaded, we can then pass in the address (&) of these objects to our first crate entity, which will be stored into the relevant pointers.

We can then optionally set a new position and rotation as well. In this case we position the crate a few units into the X axis, positioning it in front of the camera's starting position, and slightly to the left (positive Y axis). I've also added a very slight rotation in the Z axis.

We can then set up the rest of our entities, reusing the model and texture data but this time positioning them slightly differently - in this case forming a stacked pyramid of crates:

133.
    crate1.setModel(&crateModel);
134.
    crate1.setTexture(&crateTexture);
135.
    crate1.setPosition(6, 0.46, 0);
136.
    crate1.setOrientation(0, 0, 5);
137.
+ 138.
    crate2.setModel(&crateModel);
+ 139.
    crate2.setTexture(&crateTexture);
+ 140.
    crate2.setPosition(6, -0.46, 0);
+ 141.
    crate2.setOrientation(0, 0, 83);
+ 142.
+ 143.
    crate3.setModel(&crateModel);
+ 144.
    crate3.setTexture(&crateTexture);
+ 145.
    crate3.setPosition(6.03, 0, 0.7);
+ 146.
    crate3.setOrientation(0, 0, -2);
147.
148.
    SDL_SetWindowRelativeMouseMode(window, true);
149.
150.
    glClearColor(0.04f, 0.23f, 0.51f, 1.0f);

For the second crate I've (almost) added an extra 90 degrees of rotation so a different side of the cube is facing the camera - just to make the scene a bit less monotonous and artificial.

With the entities now set up, we just need to update our draw code to call the draw function of our entities:

305.
    mainShader.bind();
306.
307.
    glUniformMatrix4fv(0, 1, GL_FALSE, glm::value_ptr(pMatrix));
308.
    glUniformMatrix4fv(1, 1, GL_FALSE, glm::value_ptr(vMatrix));
309.
+ 310.
    crate1.draw();
+ 311.
    crate2.draw();
+ 312.
    crate3.draw();
313.
314.
    mainShader.unbind();
315.
316.
    SDL_GL_SwapWindow(window);
317.
}

Previously, our main.cpp was directly creating a model matrix (mMatrix), and passing this to the shaders. Of course, this is now being handled by the entities' draw functions, so we can remove it from our main draw function.

Likewise, all the model and texture binding is now being handled by the entities, so this code can also be removed from here.

Instead, this is all neatly replaced by simply telling each entity in turn to draw.

Conclusion

If you compile and run your program you should now be able to see a set of crates in front of you in your world.

Hopefully the benefits of this approach are clear!

By abstracting the models, textures, and entities, it becomes really easy for us to add in additional objects and position them within our world. Adding extra objects becomes just a matter of a few lines of code, and is reasonably efficient too! It even makes it quick and easy for us to apply different textures to each crate should we wish to.

Our world-building process is now much more straight-forward.

In the next lesson we'll take this a step further and read a configuration file to define the properties of our entities and use this to construct a full life-like world. See you there!