a-simple-triangle / Part 12 - Scenes and update loop

Although we have a working OpenGL renderer and have successfully loaded and displayed a 3D mesh, our approach is a tad rigid. We have so far hard coded our implementation directly into the OpenGLApplication class. This article will be long and is jam packed with refactoring and improvements including:

While this will be the final OpenGL based article before we delve into the Vulkan implementation, be sure to carefully follow through it to the end as some of the changes are quite significant.


Adding an update loop

Every game or interactive application will perform some set of logic operations many times per second, usually in step with the render loop. The logic operations could be anything needed to drive the behaviour of the scene. It would be fair to say that a very common operation is to perform calculations related to change over time.

As an illustrative example, let’s say we wanted to animate our lovely crate model to rotate 45 degrees per second making it appear to be spinning on the screen. To achieve this we will need the speed of rotation to be 45 degrees per second, though we know that a game will render many times per second - so how can we calculate the rotation to apply each frame? Something that should influence our answer is the fact that our game may run quicker on some computers than others due to how powerful the graphics and CPU systems are. To make sure our change over time is consistent and our crate only moves at 45 degrees per second, we need to know about the delta in time betwen subsequent render frames and apply it to our calculations. If we did not do this, on a really slow computer our crate might spin very slowly because it can’t generate frames fast enough, whereas on a really quick computer our crate might spin uncontrollably fast.

We will now add an extra feature to our base application class to perform an update every time the main loop runs, acquiring the delta in time since the previous main loop invocation completed.

Edit the main/src/application/application.hpp header - until now the application class did not store any state but we now need to store information to help with our delta calculations:

#pragma once

#include "../core/internal-ptr.hpp"

namespace ast
{
    struct Application
    {
        Application();

        virtual ~Application() = default;

        void startApplication();

        bool runMainLoop();

        virtual void update(const float& delta) = 0;

        virtual void render() = 0;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

Notice the addition of the internal field using our internal_ptr component because our application now needs to hold state. We have also added a new function signature to perform the update on each main loop - with the expectation that any subclasses implement it however they choose:

virtual void update(const float& delta) = 0;

We also changed the default constructor so it doesn’t use = default, but instead will be properly implemented in the cpp file so it can instantiate our internal field for us.

Save the header then open the main/src/application/application.cpp implementation. Directly before the startApplication function, insert the new implementation of the Internal struct:

struct Application::Internal
{
    const float performanceFrequency;
    uint64_t currentTime;
    uint64_t previousTime;

    Internal() : performanceFrequency(static_cast<float>(SDL_GetPerformanceFrequency())),
                 currentTime(SDL_GetPerformanceCounter()),
                 previousTime(currentTime) {}

    float timeStep()
    {
        previousTime = currentTime;
        currentTime = SDL_GetPerformanceCounter();

        float elapsed{(currentTime - previousTime) * 1000.0f};
        return (elapsed / performanceFrequency) * 0.001f;
    }
};

Let’s talk about this code for a bit. To evaluate the delta in time, we must hold the following fields:

By tracking both the current and previous times we have an interval against which to calculate the delta between them.

The timeStep function will perform the following duties:

The delta is returned by the timeStep function to the caller. By multiplying the delta with logical values related to speed per second we can get a frame rate independent value. In our prior example our crate needed to rotate at 45 degrees per second, so if an update cycle was run and timeStep returned a delta value of 0.002 then in the current frame update computation we should rotate our crate by 45 * 0.002 = 0.09 degrees.

Next we need to actually call the timeStep function in our main loop. Jump to our runMainLoop function and add the code to call the timeStep function then invoke the update function with the result:

bool Application::runMainLoop()
{
    ...

    // Perform our updating for this frame.
    update(internal->timeStep());

    // Perform our rendering for this frame.
    render();

    return true;
}

Lastly we need to implement the constructor instead of letting it use an auto generated default. Scroll to the bottom of the file and add the constructor implementation:

Application::Application() : internal(ast::make_internal_ptr<Internal>()) {}

Sweet, we now have the core trigger for our update cycle in place along with the delta that can be used in our game logic code elsewhere. Of course now our OpenGLApplication is broken because it doesn’t implement the update function of its super class. Edit opengl-application.hpp and add the following function signature:

void update(const float& delta) override;

Hop over to the opengl-application.cpp implementation and add the following function to the Internal struct - we will revisit this new function later when we have implemented our scene system:

struct OpenGLApplication::Internal
{
    ...

    void update(const float& delta)
    {
        // Just a stub for now ...
    }

    ...
}

Then also add the public update function implementation to the bottom of the file which delegates to the internal update function:

void OpenGLApplication::update(const float& delta)
{
    internal->update(delta);
}

Formalising our inventory of assets

Up to now we have used string literals to load .obj and .png files, for example in our OpenGL application code we have lines like this:

mesh(ast::OpenGLMesh(ast::assets::loadOBJFile("assets/models/crate.obj"))),
texture(ast::OpenGLTexture(ast::assets::loadBitmap("assets/textures/crate.png")))

While the ability to enter in string literals may seem like an advantage due to its flexibility, it can become problematic when we our collection of assets grows larger. It can also become a problem when we want to describe an asset between different layers of code in a consistent and type safe way. By modelling our assets in a more formalised way we can mitigate a lot of run time problems related to them and use them as keys when passing data around. We will augment our ast::assets namespace with some enum class declarations to give us the formalisation and compile time type safety.

Note: The trade off for formalising the inventory of our assets is that whenever an asset file is added to our project, we will need to perform a code change to expose the asset in our code base. This is totally fine for our project and the advantages are worth it for us.

Create main/src/core/asset-inventory.hpp and main/src/core/asset-inventory.cpp and enter the following into the header file:

#pragma once

namespace ast::assets
{
    enum class Pipeline
    {
        Default
    };

    enum class StaticMesh
    {
        Crate
    };

    enum class Texture
    {
        Crate
    };

    std::string resolvePipelinePath(const ast::assets::Pipeline& pipeline);

    std::string resolveStaticMeshPath(const ast::assets::StaticMesh& staticMesh);

    std::string resolveTexturePath(const ast::assets::Texture& texture);

} // namespace ast::assets

We are declaring three new enum class types:

These enumerations will be used to describe the universe of assets in our project - giving us an abstraction between pure logic and graphics API specific code. We’ll make use of these new enumerations throughout the rest of this article.

The three resolve* functions provide the mapping for what file path to use for each asset enumeration.

Edit asset-inventory.cpp adding the implementations for the resolve* free functions. Each function performs a switch on the enumeration given to it, returning the appropriate string literal file path or name in the case of the Pipeline enumeration. Other parts of our application can call these functions to find out what files to load:

#include "asset-inventory.hpp"

std::string ast::assets::resolvePipelinePath(const ast::assets::Pipeline& pipeline)
{
    switch (pipeline)
    {
        case ast::assets::Pipeline::Default:
            return "default";
    }
}

std::string ast::assets::resolveStaticMeshPath(const ast::assets::StaticMesh& staticMesh)
{
    switch (staticMesh)
    {
        case ast::assets::StaticMesh::Crate:
            return "assets/models/crate.obj";
    }
}

std::string ast::assets::resolveTexturePath(const ast::assets::Texture& texture)
{
    switch (texture)
    {
        case ast::assets::Texture::Crate:
            return "assets/textures/crate.png";
    }
}

Note that when we add more asset files into our project, we also need to create new enumerations and add their case to the appropriate switch statements to expose them into the wider code base.


Creating the asset manager

Our asset manager will actually be broken into (initially) two parts: a base AssetManager interface and an OpenGLAssetManager implementation which has OpenGL specific additions. Generally our asset manager system will perform the following duties for us:

To begin we will create the interface aspect of our asset manager. Create main/src/core/asset-manager.hpp - we won’t need an implementation just the header file. Enter the following:

#pragma once

#include "asset-inventory.hpp"
#include <vector>

namespace ast
{
    struct AssetManager
    {
        virtual void loadPipelines(const std::vector<ast::assets::Pipeline>& pipelines) = 0;

        virtual void loadStaticMeshes(const std::vector<ast::assets::StaticMesh>& staticMeshes) = 0;

        virtual void loadTextures(const std::vector<ast::assets::Texture>& textures) = 0;
    };
} // namespace ast

The AssetManager will know about our asset inventory and declare three abstract functions which must be implemented. These functions will later be used in our scene to prepare pipelines, static meshes and textures during its preparation phase. The asset manager requires a vector of each asset type be supplied rather than a single asset at a time.

OpenGL asset manager

The AssetManager header is just a contract - a pure virtual class, in some languages you might call it an interface. Next we will write the first implementation of this interface which is for our OpenGL application.

Create the files: main/src/application/opengl/opengl-asset-manager.hpp and main/src/application/opengl/opengl-asset-manager.cpp and edit the header first with the following:

#pragma once

#include "../../core/asset-manager.hpp"
#include "../../core/internal-ptr.hpp"
#include "opengl-mesh.hpp"
#include "opengl-pipeline.hpp"
#include "opengl-texture.hpp"

namespace ast
{
    struct OpenGLAssetManager : public ast::AssetManager
    {
        OpenGLAssetManager();

        void loadPipelines(const std::vector<ast::assets::Pipeline>& pipelines) override;

        void loadStaticMeshes(const std::vector<ast::assets::StaticMesh>& staticMeshes) override;

        void loadTextures(const std::vector<ast::assets::Texture>& textures) override;

        const ast::OpenGLPipeline& getPipeline(const ast::assets::Pipeline& pipeline) const;

        const ast::OpenGLMesh& getStaticMesh(const ast::assets::StaticMesh& staticMesh) const;

        const ast::OpenGLTexture& getTexture(const ast::assets::Texture& texture) const;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

You can see that we are subclassing the AssetManager interface like this:

struct OpenGLAssetManager : public ast::AssetManager

We are also declaring the functions that the interface requires us to implement, indicating that we are overriding them:

void loadPipelines(const std::vector<ast::assets::Pipeline>& pipelines) override;

void loadStaticMeshes(const std::vector<ast::assets::StaticMesh>& staticMeshes) override;

void loadTextures(const std::vector<ast::assets::Texture>& textures) override;

Our OpenGL version will also supply some additional functions which are not in the interface. This is to accommodate the specific OpenGL API requirements needed to fulfill our OpenGL use cases. Notice how these functions return constant references to the OpenGL specific types that represent our asset inventory enumerations. For example if we asked for the ast::assets::Pipeline enumeration, a concrete ast::OpenGLPipeline object will be returned.

const ast::OpenGLPipeline& getPipeline(const ast::assets::Pipeline& pipeline) const;

const ast::OpenGLMesh& getStaticMesh(const ast::assets::StaticMesh& staticMesh) const;

const ast::OpenGLTexture& getTexture(const ast::assets::Texture& texture) const;

We are also using our internal_ptr component here because our asset manager will need to store state in order to cache the assets. Save the header and open the implementation file. Enter the following, which is quite long but not terribly complicated:

#include "opengl-asset-manager.hpp"
#include "../../core/assets.hpp"
#include <unordered_map>

using ast::OpenGLAssetManager;

struct OpenGLAssetManager::Internal
{
    std::unordered_map<ast::assets::Pipeline, ast::OpenGLPipeline> pipelineCache;
    std::unordered_map<ast::assets::StaticMesh, ast::OpenGLMesh> staticMeshCache;
    std::unordered_map<ast::assets::Texture, ast::OpenGLTexture> textureCache;

    Internal() {}

    void loadPipelines(const std::vector<ast::assets::Pipeline>& pipelines)
    {
        for (const auto& pipeline : pipelines)
        {
            if (pipelineCache.count(pipeline) == 0)
            {
                pipelineCache.insert(std::make_pair(
                    pipeline,
                    ast::OpenGLPipeline(ast::assets::resolvePipelinePath(pipeline))));
            }
        }
    }

    void loadStaticMeshes(const std::vector<ast::assets::StaticMesh>& staticMeshes)
    {
        for (const auto& staticMesh : staticMeshes)
        {
            if (staticMeshCache.count(staticMesh) == 0)
            {
                staticMeshCache.insert(std::make_pair(
                    staticMesh,
                    ast::OpenGLMesh(ast::assets::loadOBJFile(ast::assets::resolveStaticMeshPath(staticMesh)))));
            }
        }
    }

    void loadTextures(const std::vector<ast::assets::Texture>& textures)
    {
        for (const auto& texture : textures)
        {
            if (textureCache.count(texture) == 0)
            {
                textureCache.insert(std::pair(
                    texture,
                    ast::OpenGLTexture(ast::assets::loadBitmap(ast::assets::resolveTexturePath(texture)))));
            }
        }
    }
};

OpenGLAssetManager::OpenGLAssetManager() : internal(ast::make_internal_ptr<Internal>()) {}

void OpenGLAssetManager::loadPipelines(const std::vector<ast::assets::Pipeline>& pipelines)
{
    internal->loadPipelines(pipelines);
}

void OpenGLAssetManager::loadStaticMeshes(const std::vector<ast::assets::StaticMesh>& staticMeshes)
{
    internal->loadStaticMeshes(staticMeshes);
}

void OpenGLAssetManager::loadTextures(const std::vector<ast::assets::Texture>& textures)
{
    internal->loadTextures(textures);
}

const ast::OpenGLPipeline& OpenGLAssetManager::getPipeline(const ast::assets::Pipeline& pipeline) const
{
    return internal->pipelineCache.at(pipeline);
}

const ast::OpenGLMesh& OpenGLAssetManager::getStaticMesh(const ast::assets::StaticMesh& staticMesh) const
{
    return internal->staticMeshCache.at(staticMesh);
}

const ast::OpenGLTexture& OpenGLAssetManager::getTexture(const ast::assets::Texture& texture) const
{
    return internal->textureCache.at(texture);
}

We will start by examining the Internal struct first - a lot of the public function implementations simply delegate to it. First up we are declaring some hash map data structures using the std::unordered_map component which acts as the storage for our caching. Notice that the key in each map is an asset inventory enumeration, and the value is the concrete OpenGL instance of the object:

struct OpenGLAssetManager::Internal
{
    std::unordered_map<ast::assets::Pipeline, ast::OpenGLPipeline> pipelineCache;
    std::unordered_map<ast::assets::StaticMesh, ast::OpenGLMesh> staticMeshCache;
    std::unordered_map<ast::assets::Texture, ast::OpenGLTexture> textureCache;

   ...

Next we have the code to fetch and create the Pipeline types. We have the internal loadPipelines function which iterates a list of Pipeline types, checking for an entry in the pipelineCache map and if one wasn’t found, creates a new instance of an ast::OpenGLPipeline using the resolvePipelinePath free function we wrote earlier. The new instance is then stored in our cache:

void loadPipelines(const std::vector<ast::assets::Pipeline>& pipelines)
{
    for (const auto& pipeline : pipelines)
    {
        if (pipelineCache.count(pipeline) == 0)
        {
            pipelineCache.insert(std::make_pair(
                pipeline,
                ast::OpenGLPipeline(ast::assets::resolvePipelinePath(pipeline))));
        }
    }
}

The stored instance of a Pipeline enumeration is fetched through the getPipeline function, basically it just returns the concrete instance stored in the cache:

const ast::OpenGLPipeline& OpenGLAssetManager::getPipeline(const ast::assets::Pipeline& pipeline) const
{
    return internal->pipelineCache.at(pipeline);
}

You may have noticed a design decision I’ve made with our asset manager - if we try to get an asset that has not yet been loaded our application will crash. I made this choice deliberately to force consumers of an asset manager to load all their assets before accessing them, rather than having them load on demand as they are fetched. By conforming to this requirement we keep tight control on when assets are loaded - allowing us in the future to implement loading phases and apply optimisations of how API specific assets are handled - for example if all the static meshes are loaded at once we might actually be able to bundle them all into a single memory buffer instead of many individual ones, or if all textures were loaded at once we might be able to apply some kind of optimisations to them such as bin packing to merge them into fewer larger textures to minimise texture binding calls.

The takeway is that a consumer must have loaded any assets it needs through the load... functions before using them.

I won’t walk through the rest of the code in opengl-asset-manager.cpp - it is largely the same kind of thing as the pipeline code we just examined, but for static meshes and textures instead.


Creating the static mesh instance class

Imagine we wanted to display two crates in our 3D world instead of just one like we are at the moment. Without any additional code changes we would need to do something like the following in our OpenGL application:

const ast::OpenGLMesh mesh;
const ast::OpenGLTexture texture;

const glm::mat4 meshTransform1; // First crate
const glm::mat4 meshTransform2; // Second crate

...
const glm::mat4 mvp1{
    camera.getProjectionMatrix() *
    camera.getViewMatrix() *
    meshTransform1};

defaultPipeline.render(mesh, texture, mvp1);

const glm::mat4 mvp2{
    camera.getProjectionMatrix() *
    camera.getViewMatrix() *
    meshTransform2};

defaultPipeline.render(mesh, texture, mvp2);

This approach won’t scale very nicely and doesn’t offer us a single simplistic view of an instance of a 3D model in our world - instead we just have a bunch of disparate fields that are only cohesively useful when pulled together manually during the render phase. We would also like to shift the declaration of all our 3D models into a scene which shouldn’t know about OpenGL or Vulkan but could abstractly know about the objects it contains keeping it API agnostic.

What might be nicer is to have a class that completely represents a static mesh instance in our world like this:

.:: StaticMeshInstance ::.

-> Position, scale and rotation
-> Which asset inventory static mesh to use
-> Which asset inventory texture to use
-> Transformation matrix updated every update cycle

Once we have this new class, we could easily create multiple actors like this (pretend our application is about fruit or something):

StaticMeshInstance apple{StaticMesh::Apple, Texture::Apple};
StaticMeshInstance orange{StaticMesh::Orange, Texture::Orange};
StaticMeshInstance banana{StaticMesh::Banana, Texture::Banana};

The instances themselves don’t know anything about how to load meshes or textures or have any awareness of OpenGL etc. The responsibility for managing those underlying assets will fall to the asset manager and the application implementation.

Note: There are other ways to represent entities in a system instead of creating classes with properties and functions like what we are about to create. A popular architecture pattern is the Entity Component System (ECS) which is likely to be more scalable and flexible at the cost of code complexity. We will keep the code in this series simple and go for a more basic approach. I would however highly recommend studying the ECS pattern as it is a very interesting mind shift from typical object oriented thinking - kind of like a super charged composition over inheritance pattern.

Let’s get cracking with our static mesh instance class - create main/src/core/static-mesh-instance.hpp and main/src/core/static-mesh-instance.cpp. Edit the header file with the following:

#pragma once

#include "asset-inventory.hpp"
#include "glm-wrapper.hpp"
#include "internal-ptr.hpp"

namespace ast
{
    struct StaticMeshInstance
    {
        StaticMeshInstance(const ast::assets::StaticMesh& staticMesh,
                           const ast::assets::Texture& texture,
                           const glm::vec3& position = glm::vec3{0.0f, 0.0f, 0.0f},
                           const glm::vec3& scale = glm::vec3{1.0f, 1.0f, 1.0f},
                           const glm::vec3& rotationAxis = glm::vec3{0.0f, 1.0f, 0.0f},
                           const float& rotationDegrees = 0.0f);

        void update(const glm::mat4& projectionViewMatrix);

        void rotateBy(const float& degrees);

        ast::assets::StaticMesh getMesh() const;

        ast::assets::Texture getTexture() const;

        glm::mat4 getTransformMatrix() const;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

We are defining an object that can hold a definition of which mesh and texture to use and offer a way to update and fetch its transform matrix. We need the transform matrix to know where and how the mesh should be positioned in the 3d world.

Our instance will hold information about its transformation including its position, scale, rotation axis and rotation amount. We will add the rotateBy function to rotate the instance - in the future we could add more mutation functions to manipulate other transform properties. We will use default constructor arguments to allow an instance to be created using default values or by specifying them explicitly.

Edit the implementation to the following:

#include "static-mesh-instance.hpp"

using ast::StaticMeshInstance;

struct StaticMeshInstance::Internal
{
    const ast::assets::StaticMesh mesh;
    const ast::assets::Texture texture;
    const glm::mat4 identity;

    glm::vec3 position;
    glm::vec3 scale;
    glm::vec3 rotationAxis;
    float rotationDegrees;
    glm::mat4 transformMatrix;

    Internal(const ast::assets::StaticMesh& mesh,
             const ast::assets::Texture& texture,
             const glm::vec3& position,
             const glm::vec3& scale,
             const glm::vec3& rotationAxis,
             const float& rotationDegrees)
        : mesh(mesh),
          texture(texture),
          identity(glm::mat4{1.0f}),
          position(position),
          scale(scale),
          rotationAxis(rotationAxis),
          rotationDegrees(rotationDegrees),
          transformMatrix(identity) {}

    void update(const glm::mat4& projectionViewMatrix)
    {
        transformMatrix = projectionViewMatrix *
                          glm::translate(identity, position) *
                          glm::rotate(identity, glm::radians(rotationDegrees), rotationAxis) *
                          glm::scale(identity, scale);
    }

    void rotateBy(const float& degrees)
    {
        rotationDegrees += degrees;

        if (rotationDegrees > 360.0f)
        {
            rotationDegrees -= 360.0f;
        }
        else if (rotationDegrees < -360.0f)
        {
            rotationDegrees += 360.0f;
        }
    }
};

StaticMeshInstance::StaticMeshInstance(
    const ast::assets::StaticMesh& staticMesh,
    const ast::assets::Texture& texture,
    const glm::vec3& position,
    const glm::vec3& scale,
    const glm::vec3& rotationAxis,
    const float& rotationDegrees)
    : internal(ast::make_internal_ptr<Internal>(
          staticMesh,
          texture,
          position,
          scale,
          rotationAxis,
          rotationDegrees)) {}

void StaticMeshInstance::update(const glm::mat4& projectionViewMatrix)
{
    internal->update(projectionViewMatrix);
}

void StaticMeshInstance::rotateBy(const float& degrees)
{
    internal->rotateBy(degrees);
}

ast::assets::StaticMesh StaticMeshInstance::getMesh() const
{
    return internal->mesh;
}

ast::assets::Texture StaticMeshInstance::getTexture() const
{
    return internal->texture;
}

glm::mat4 StaticMeshInstance::getTransformMatrix() const
{
    return internal->transformMatrix;
}

Our implementation holds the fields passed into the constructor so they can be used when calculating the transform matrix and advertise which static mesh and texture should be used when integrating the instance in our application. We also create and hold the identity matrix as we need to use it frequently.

We will be updating each instance on every frame to apply the current transformation matrix of the perspective camera combined with the transformation properties of the instance. The bulk of this function already existed in our opengl-application.cpp file, the only difference here being that the instance maintains its own internal transform matrix state. For a refresher on matrices for 3D this is a good article: https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices.

void update(const glm::mat4& projectionViewMatrix)
{
    transformMatrix = projectionViewMatrix *
                      glm::translate(identity, position) *
                      glm::rotate(identity, glm::radians(rotationDegrees), rotationAxis) *
                      glm::scale(identity, scale);
}

We also have the rotateBy function which simply mutates the rotationDegrees field, wrapping it around the 360 degrees limits.

That’s it for the static mesh instance class for now - we can enhance it later as we need to.


Creating a rendering contract

Our current OpenGL application hard codes the way it performs the rendering each frame with code like this:

struct OpenGLApplication::Internal
{
   ...

    void render()
    {
        SDL_GL_MakeCurrent(window, context);

        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        const glm::mat4 mvp{
            camera.getProjectionMatrix() *
            camera.getViewMatrix() *
            meshTransform};

        defaultPipeline.render(mesh, texture, mvp);

        SDL_GL_SwapWindow(window);
    }

   ...

We’ve already established that this is far too rigid for us and requires a lot of tightly coupled fields interacting with each other directly in the OpenGL implementation. We need to model a better, more abstract way of rendering so our scene system can perform rendering without knowing about OpenGL or Vulkan.

The approach we will take to solve this problem is to form a new interface that represents rendering tasks that a scene can invoke - but without any API specific code. We will then write an OpenGL specific implementation of the interface as the backing for it in our OpenGL application.

Create a header file main/src/core/renderer.hpp and enter the following into it:

#pragma once

#include "asset-inventory.hpp"
#include "static-mesh-instance.hpp"
#include <vector>

namespace ast
{
    struct Renderer
    {
        virtual void render(
            const ast::assets::Pipeline& pipeline,
            const std::vector<ast::StaticMeshInstance>& staticMeshInstances) = 0;
    };
} // namespace ast

The Renderer interface is pure virtual and at the moment offers only one function to request a specific Pipeline to render a list of StaticMeshInstance objects. There is no default implementation of this interface - each API must implement their own version.

Add main/src/application/opengl/opengl-renderer.hpp and main/src/application/opengl/opengl-renderer.cpp which will become our OpenGL implementation of the renderer. Edit the header file with the following:

#pragma once

#include "../../core/internal-ptr.hpp"
#include "../../core/renderer.hpp"
#include "opengl-asset-manager.hpp"
#include <memory>

namespace ast
{
    struct OpenGLRenderer : public ast::Renderer
    {
        OpenGLRenderer(std::shared_ptr<ast::OpenGLAssetManager> assetManager);

        void render(
            const ast::assets::Pipeline& pipeline,
            const std::vector<ast::StaticMeshInstance>& staticMeshInstances) override;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

Observe that the OpenGLRenderer implements the ast::Renderer interface, therefore needs to declare the render function with the override syntax. We also need to pass in the OpenGL asset manager so the renderer can use it which will do via a shared_ptr.

Edit opengl-renderer.cpp with the following:

#include "opengl-renderer.hpp"

using ast::OpenGLRenderer;

struct OpenGLRenderer::Internal
{
    const std::shared_ptr<ast::OpenGLAssetManager> assetManager;

    Internal(std::shared_ptr<ast::OpenGLAssetManager> assetManager) : assetManager(assetManager) {}

    void render(
        const ast::assets::Pipeline& pipeline,
        const std::vector<ast::StaticMeshInstance>& staticMeshInstances)
    {
        assetManager->getPipeline(pipeline).render(*assetManager, staticMeshInstances);
    }
};

OpenGLRenderer::OpenGLRenderer(std::shared_ptr<ast::OpenGLAssetManager> assetManager)
    : internal(ast::make_internal_ptr<Internal>(assetManager)) {}

void OpenGLRenderer::render(
    const ast::assets::Pipeline& pipeline,
    const std::vector<ast::StaticMeshInstance>& staticMeshInstances)
{
    internal->render(pipeline, staticMeshInstances);
}

Our Internal struct holds onto the asset manager instance via a shared_ptr and implements the render function. Note that at the moment the following line of code will show syntax errors:

assetManager.getPipeline(pipeline).render(*assetManager, staticMeshInstances);

The reason is that we need to revisit our OpenGL pipeline class to change its own render function to accept a list of static mesh instances and an asset manager. There isn’t much more to the OpenGL renderer class. Let’s fix up our OpenGLPipeline class now so it can receive our static meshes for rendering.

Edit opengl-pipeline.hpp first, changing the signature of the render function and updating the required header includes:

#pragma once

#include "../../core/internal-ptr.hpp"
#include "../../core/static-mesh-instance.hpp"
#include <string>
#include <vector>

namespace ast
{
    struct OpenGLAssetManager;

    struct OpenGLPipeline
    {
        OpenGLPipeline(const std::string& shaderName);

        void render(
            const ast::OpenGLAssetManager& assetManager,
            const std::vector<ast::StaticMeshInstance>& staticMeshInstances) const;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

Note that the signature of the render function has been changed to accept an OpenGLAssetManager and a list of StaticMeshInstance objects. There is one slight quirk in this header - we have had to forward declare the asset manager with this line:

struct OpenGLAssetManager;

If we did not do this, our code would not compile because it would form a circular dependency where OpenGLAssetManager depends on OpenGLPipeline but OpenGLPipeline also depends on OpenGLAssetManager. By forward declaring we can avoid this problem.

Note: Another approach would be to extrude the common requirements between the circular classes into a third intermediary class however I’ll just use a forward declaration for now.

Open opengl-pipeline.cpp to update the implementation. First off, add the following header file to pull in the asset manager:

#include "opengl-asset-manager.hpp"

Then update the public function implementation of the render function at the bottom of the source file:

void OpenGLPipeline::render(const ast::OpenGLMesh& mesh, const ast::OpenGLTexture& texture, const glm::mat4& mvp) const
{
    internal->render(mesh, texture, mvp);
}

Becomes:

void OpenGLPipeline::render(
    const ast::OpenGLAssetManager& assetManager,
    const std::vector<ast::StaticMeshInstance>& staticMeshInstances) const
{
    internal->render(assetManager, staticMeshInstances);
}

We then need to refactor the render function in the Internal struct. Replace the entire existing internal render function with the following:

void render(
    const ast::OpenGLAssetManager& assetManager,
    const std::vector<ast::StaticMeshInstance>& staticMeshInstances) const
{
    // Instruct OpenGL to starting using our shader program.
    glUseProgram(shaderProgramId);

    // Enable the 'a_vertexPosition' attribute.
    glEnableVertexAttribArray(attributeLocationVertexPosition);

    // Enable the 'a_texCoord' attribute.
    glEnableVertexAttribArray(attributeLocationTexCoord);

    for (const auto& staticMeshInstance : staticMeshInstances)
    {
        const ast::OpenGLMesh& mesh = assetManager.getStaticMesh(staticMeshInstance.getMesh());

        // Populate the 'u_mvp' uniform in the shader program.
        glUniformMatrix4fv(uniformLocationMVP, 1, GL_FALSE, &staticMeshInstance.getTransformMatrix()[0][0]);

        // Apply the texture we want to paint the mesh with.
        assetManager.getTexture(staticMeshInstance.getTexture()).bind();

        // Bind the vertex and index buffers.
        glBindBuffer(GL_ARRAY_BUFFER, mesh.getVertexBufferId());
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.getIndexBufferId());

        // Configure the 'a_vertexPosition' attribute.
        glVertexAttribPointer(
            attributeLocationVertexPosition,
            3,
            GL_FLOAT,
            GL_FALSE,
            stride,
            reinterpret_cast<const GLvoid*>(offsetPosition));

        // Configure the 'a_texCoord' attribute.
        glVertexAttribPointer(attributeLocationTexCoord,
            2,
            GL_FLOAT,
            GL_FALSE,
            stride,
            reinterpret_cast<const GLvoid*>(offsetTexCoord));

        // Execute the draw command - with how many indices to iterate.
        glDrawElements(
            GL_TRIANGLES,
            mesh.getNumIndices(),
            GL_UNSIGNED_INT,
            reinterpret_cast<const GLvoid*>(0));
    }

    // Tidy up.
    glDisableVertexAttribArray(attributeLocationVertexPosition);
    glDisableVertexAttribArray(attributeLocationTexCoord);
}

Quite a bit of the code is the same as the previous render function except for some ordering of commands and that now we will iterate through each of the static mesh instances given to us. The first three things that we do are to activate the shader program and enable the a_vertexPosition and a_texCoord attributes in the shader:

// Instruct OpenGL to starting using our shader program.
glUseProgram(shaderProgramId);

// Enable the 'a_vertexPosition' attribute.
glEnableVertexAttribArray(attributeLocationVertexPosition);

// Enable the 'a_texCoord' attribute.
glEnableVertexAttribArray(attributeLocationTexCoord);

Important: Observe that we are no longer calling the glVertexAttribPointer commands in the same place as before - they must be executed for each mesh in our loop code otherwise we will get corrupted rendering with more than one mesh.

We take the list of static mesh instances and iterate over them:

for (const auto& staticMeshInstance : staticMeshInstances)
{
    ...
}

For each static mesh instance, we obtain its actual concrete object in memory through the asset manager - remembering that the asset should have been loaded long before we end up in this rendering code:

const ast::OpenGLMesh& mesh = assetManager.getStaticMesh(staticMeshInstance.getMesh());

We then obtain the transformation matrix that should be inflated into the u_mvp uniform by asking the mesh instance itself for it then forward it into the shader uniform as before:

// Populate the 'u_mvp' uniform in the shader program.
glUniformMatrix4fv(uniformLocationMVP, 1, GL_FALSE, &staticMeshInstance.getTransformMatrix()[0][0]);

Next we find out what texture the instance needs to paint itself with, again asking the provided asset manager for it then subsequently binding it into the shader program:

// Apply the texture we want to paint the mesh with.
assetManager.getTexture(staticMeshInstance.getTexture()).bind();

As before, the vertex and index buffers are bound to the pipeline to become the source of input to the shader program:

// Bind the vertex and index buffers.
glBindBuffer(GL_ARRAY_BUFFER, mesh.getVertexBufferId());
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.getIndexBufferId());

After the buffers have been bound, we configure the vertex position and texture coordinate shader attributes through the same commands as we did before except it must be done at this point in the ordering of the rendering code to work correctly:

// Configure the 'a_vertexPosition' attribute.
glVertexAttribPointer(
    attributeLocationVertexPosition,
    3,
    GL_FLOAT,
    GL_FALSE,
    stride,
    reinterpret_cast<const GLvoid*>(offsetPosition));

// Configure the 'a_texCoord' attribute.
glVertexAttribPointer(attributeLocationTexCoord,
    2,
    GL_FLOAT,
    GL_FALSE,
    stride,
    reinterpret_cast<const GLvoid*>(offsetTexCoord));

The last command in the loop is to call the draw command which we already had before to cause the current static mesh instance to be rendered:

// Execute the draw command - with how many indices to iterate.
glDrawElements(
    GL_TRIANGLES,
    mesh.getNumIndices(),
    GL_UNSIGNED_INT,
    reinterpret_cast<const GLvoid*>(0));

Observe that we have changed instances of (GLvoid*)(...) to reinterpret_cast<const GLvoid*>(...) - this resolves the C++ warnings you may have noticed when compiling this code.

When the loop has completed we perform our tidy up steps as before:

// Tidy up.
glDisableVertexAttribArray(attributeLocationVertexPosition);
glDisableVertexAttribArray(attributeLocationTexCoord);

Note: There are optimisation techniques that are recommended when rendering multiple meshes and textures. To keep things simple at the moment I won’t be applying these however you can do further reading here https://www.khronos.org/opengl/wiki/Vertex_Specification_Best_Practices. Also the act of binding textures or just generally making pipeline state changes is considered expensive. For texturing you could arrange the ordering of your meshes so they are grouped by texture to minimise texture bindings etc.

We now have an OpenGL renderer, along with an interface that can invoke our rendering code in an agnostic way - perfect for consumption in our scene system. Note that if you try to run your application now you will get compiler errors because we haven’t yet updated our main OpenGL application code. Before updating our application code we will make one more detour and create these scene classes that we keep talking about …


Creating a scene

Think of a scene as the model which drives the content we want to display, along with the logic behind the rules of behaviour to apply over time. Our scene will strive to be graphics platform agnostic by using our new asset inventory and static mesh instance classes to represent objects to display and their configurations.

Eventually we would want to define many scenes in our project, so we will start by creating a base scene class to define the common functions that every scene should have. Create a new folder named main/src/scene then create a new header file named scene.hpp in that folder. Enter the following to define our basic scene shape:

#pragma once

#include "../core/asset-manager.hpp"
#include "../core/renderer.hpp"

namespace ast
{
    struct Scene
    {
        Scene() = default;

        virtual ~Scene() = default;

        virtual void prepare(ast::AssetManager& assetManager) = 0;

        virtual void update(const float& delta) = 0;

        virtual void render(ast::Renderer& renderer) = 0;
    };
} // namespace ast

The scene.hpp forms an interface as it is a pure virtual class. The interesting functions are:

There is no default implementation of a scene, each concrete scene needs to implement the interface functions. Let’s author our first scene that should be our main scene when the application starts.

Create main/src/scene/scene-main.hpp and main/src/scene/scene-main.cpp, entering the following into the header file:

#pragma once

#include "../core/internal-ptr.hpp"
#include "scene.hpp"

namespace ast
{
    struct SceneMain : public ast::Scene
    {
        SceneMain(const float& screenWidth, const float& screenHeight);

        void prepare(ast::AssetManager& assetManager) override;

        void update(const float& delta) override;

        void render(ast::Renderer& renderer) override;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

Our main scene will need to be initialised with the width and height of the screen because internally it will create its own perspective camera. Observe also that we are implementing the ast::Scene interface, so we need to declare all the functions from that interface with the override qualifier.

Edit scene-main.cpp with the following:

#include "scene-main.hpp"
#include "../core/perspective-camera.hpp"
#include "../core/static-mesh-instance.hpp"

using ast::SceneMain;
using ast::assets::Pipeline;
using ast::assets::StaticMesh;
using ast::assets::Texture;

namespace
{
    ast::PerspectiveCamera createCamera(const float& width, const float& height)
    {
        return ast::PerspectiveCamera(width, height);
    }
} // namespace

struct SceneMain::Internal
{
    ast::PerspectiveCamera camera;
    std::vector<ast::StaticMeshInstance> staticMeshes;

    Internal(const float& screenWidth, const float& screenHeight)
        : camera(::createCamera(screenWidth, screenHeight)) {}

    void prepare(ast::AssetManager& assetManager)
    {
        assetManager.loadPipelines({Pipeline::Default});
        assetManager.loadStaticMeshes({StaticMesh::Crate});
        assetManager.loadTextures({Texture::Crate});

        staticMeshes.push_back(ast::StaticMeshInstance{StaticMesh::Crate, Texture::Crate});
    }

    void update(const float& delta)
    {
        const glm::mat4 cameraMatrix{camera.getProjectionMatrix() * camera.getViewMatrix()};

        for (auto& staticMesh : staticMeshes)
        {
            staticMesh.rotateBy(delta * 45.0f);
            staticMesh.update(cameraMatrix);
        }
    }

    void render(ast::Renderer& renderer)
    {
        renderer.render(Pipeline::Default, staticMeshes);
    }
};

SceneMain::SceneMain(const float& screenWidth, const float& screenHeight)
    : internal(ast::make_internal_ptr<Internal>(screenWidth, screenHeight)) {}

void SceneMain::prepare(ast::AssetManager& assetManager)
{
    internal->prepare(assetManager);
}

void SceneMain::update(const float& delta)
{
    internal->update(delta);
}

void SceneMain::render(ast::Renderer& renderer)
{
    internal->render(renderer);
}

Our main scene implementation is responsible for defining all the mesh instances that should be presented and will also need to maintain its own perspective camera. We hold and initialise the camera and staticMeshes fields in the Internal struct to do this. Note that the createCamera free function was taken almost directly from our OpenGL application code so I won’t explain it here:

struct SceneMain::Internal
{
    ast::PerspectiveCamera camera;
    std::vector<ast::StaticMeshInstance> staticMeshes;

    Internal(const float& screenWidth, const float& screenHeight)
        : camera(::createCamera(screenWidth, screenHeight)) {}

   ...

The first interesting function puts into practice many of the pieces we’ve introduced in this article by using the AssetManager interface to load all the assets this scene requires. It also sets up the the static meshes we want to present in the render phase:

void prepare(ast::AssetManager& assetManager)
{
    assetManager.loadPipelines({Pipeline::Default});
    assetManager.loadStaticMeshes({StaticMesh::Crate});
    assetManager.loadTextures({Texture::Crate});

    staticMeshes.push_back(ast::StaticMeshInstance{StaticMesh::Crate, Texture::Crate});
}

If we wanted to add more meshes to our scene, we would simply call staticMeshes.push_back(ast::StaticMeshInstance...) as many times as we desire.

The next interesting function is the update implementation, which will be triggered every frame before rendering occurs. The update cycle will basically do the following:

void update(const float& delta)
{
    const glm::mat4 cameraMatrix{camera.getProjectionMatrix() * camera.getViewMatrix()};

    for (auto& staticMesh : staticMeshes)
    {
        staticMesh.rotateBy(delta * 45.0f);
        staticMesh.update(cameraMatrix);
    }
}

This function is also the place where we would put any other scene based logic, for example checking for input events or applying other behavioural rules. It might also be the place where the list of what to render is evaluated to perform view culling etc.

The final function of interest (they are all interesting though aren’t they!?) we have the render function. This is the place that directs the provided Renderer on what to present to the screen. Our scene uses the Default shader pipeline and sends all of its static meshes into it for rendering. The underlying renderer implementation will handle the actual presentation.

void render(ast::Renderer& renderer)
{
    renderer.render(Pipeline::Default, staticMeshes);
}

Updating the OpenGL application

All these new bits and pieces are starting to coalesce - the final thing we need to do is update our main OpenGL application to make use of our new scene and get rid of the hard coded rendering and asset integrations.

Open the opengl-application.cpp file - we’ll be making a series of changes including the removal of a bunch of code. Start by changing the set of header includes from this:

#include "opengl-application.hpp"
#include "../../core/assets.hpp"
#include "../../core/graphics-wrapper.hpp"
#include "../../core/log.hpp"
#include "../../core/perspective-camera.hpp"
#include "../../core/sdl-wrapper.hpp"
#include "opengl-mesh.hpp"
#include "opengl-pipeline.hpp"
#include "opengl-texture.hpp"
#include <string>

to this:

#include "opengl-application.hpp"
#include "../../core/graphics-wrapper.hpp"
#include "../../core/log.hpp"
#include "../../core/sdl-wrapper.hpp"
#include "../../scene/scene-main.hpp"
#include "opengl-asset-manager.hpp"
#include "opengl-renderer.hpp"

Note the addition of the scene, asset manager and renderer headers and the removal of the perspective camera, mesh, pipeline and texture headers.

Next, delete the following free functions from the anonymous namespace as we will no longer need them:

// Delete this function
ast::PerspectiveCamera createCamera()
{
    std::pair<uint32_t, uint32_t> displaySize{ast::sdl::getDisplaySize()};

    return ast::PerspectiveCamera(static_cast<float>(displaySize.first), static_cast<float>(displaySize.second));
}

// Delete this function
glm::mat4 createMeshTransform()
{
    glm::mat4 identity{1.0f};
    glm::vec3 position{0.0f, 0.0f, 0.0f};
    glm::vec3 rotationAxis{0.0f, 1.0f, 0.0f};
    glm::vec3 scale{1.0f, 1.0f, 1.0f};
    float rotationDegrees{45.0f};

    return glm::translate(identity, position) *
           glm::rotate(identity, glm::radians(rotationDegrees), rotationAxis) *
           glm::scale(identity, scale);
}

In their place, add three new free functions into the anonymous namespace which we’ll use during application construction:

std::shared_ptr<ast::OpenGLAssetManager> createAssetManager()
{
    return std::make_shared<ast::OpenGLAssetManager>(ast::OpenGLAssetManager());
}

ast::OpenGLRenderer createRenderer(std::shared_ptr<ast::OpenGLAssetManager> assetManager)
{
    return ast::OpenGLRenderer(assetManager);
}

std::unique_ptr<ast::Scene> createMainScene(ast::AssetManager& assetManager)
{
    std::pair<uint32_t, uint32_t> displaySize{ast::sdl::getDisplaySize()};
    std::unique_ptr<ast::Scene> scene{std::make_unique<ast::SceneMain>(
        static_cast<float>(displaySize.first),
        static_cast<float>(displaySize.second))};
    scene->prepare(assetManager);
    return scene;
}

The createAssetManager and createRenderer functions are not very special - perhaps the only noteworthy comment is that the asset manager will be wrapped in a std::shared_ptr so its ownership can respect the shared ownership semantic. This is useful to us because we pass the asset manager into other objects during initialisation who might want to hold a reference to it such as the renderer.

The createMainScene will construct a new instance of the ast::SceneMain class but inside a std::unique_ptr of type ast::Scene so it can be used polymorphically as a basic scene. You might also spot the following incantation before the new scene is returned from the function which causes our scene to load all of its assets and get itself ready for execution:

scene->prepare(assetManager);

Jump down to the Internal struct and change the member fields and constructor from this:

struct OpenGLApplication::Internal
{
    SDL_Window* window;
    SDL_GLContext context;
    const ast::PerspectiveCamera camera;
    const ast::OpenGLPipeline defaultPipeline;
    const ast::OpenGLMesh mesh;
    const glm::mat4 meshTransform;
    const ast::OpenGLTexture texture;

    Internal() : window(ast::sdl::createWindow(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE)),
                 context(::createContext(window)),
                 camera(::createCamera()),
                 defaultPipeline(ast::OpenGLPipeline("default")),
                 mesh(ast::OpenGLMesh(ast::assets::loadOBJFile("assets/models/crate.obj"))),
                 meshTransform(::createMeshTransform()),
                 texture(ast::OpenGLTexture(ast::assets::loadBitmap("assets/textures/crate.png"))) {}

   ...

to this:

struct OpenGLApplication::Internal
{
    SDL_Window* window;
    SDL_GLContext context;
    const std::shared_ptr<ast::OpenGLAssetManager> assetManager;
    ast::OpenGLRenderer renderer;
    std::unique_ptr<ast::Scene> scene;

    Internal() : window(ast::sdl::createWindow(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE)),
                 context(::createContext(window)),
                 assetManager(::createAssetManager()),
                 renderer(::createRenderer(assetManager)) {}

   ...   

Observe that we now create and hold instances of an OpenGLAssetManager (via a std::shared_ptr), OpenGLRenderer and a std::unique_ptr container for a scene. We don’t initialise the scene in the constructor of the main application, preferring to leave it to be lazily instantiated. Add the following function inside the Internal struct to lazily instantiate our main scene if it needs to be - I explained the createMainScene free function a moment ago:

struct OpenGLApplication::Internal
{
    ...

    ast::Scene& getScene()
    {
        if (!scene)
        {
            scene = ::createMainScene(assetManager);
        }

        return *scene;
    }

    ...
}

With our getScene function defined we can now revisit our update function and ask our scene to update itself:

struct OpenGLApplication::Internal
{
    ...

    void update(const float& delta)
    {
        getScene().update(delta);
    }

    ...
}

Finally, we can fix our render function to use our new scene:

struct OpenGLApplication::Internal
{
    ...

    void render()
    {
        SDL_GL_MakeCurrent(window, context);

        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        getScene().render(renderer);

        SDL_GL_SwapWindow(window);
    }

    ...
}

Observe that our main application doesn’t do much at all now in the render loop, it simply gets the current scene and asks it to render itself, passing it an instance of a Renderer interface - which in the OpenGL application is the OpenGLRenderer member field.

Now if you followed along carefully you should be able to run the application again and see our favourite geometric object spinning around in the middle of the screen!


Moar mesh instances!!

To wrap up let’s add a new mesh asset and texture image and push a few more mesh instances into our main scene with differing positions, scales and rotation axes.

Download and save torus.obj into your main/assets/models folder. Also right click the totally radical texture image below and save it as main/assets/textures/red_cross_hatch.png:

Important: Be aware that asset files on Android must only contain alpha numeric characters and underscores _ as separators. You cannot use dashes - or spaces etc so name your texture files lower case with underscores if you want to be safe.

To add our new torus.obj and red_cross_hatch.png assets, revisit our asset-inventory.hpp header and add the new enumerations StaticMesh::Torus and Texture::RedCrossHatch:

enum class StaticMesh
{
    Crate,
    Torus
};

enum class Texture
{
    Crate,
    RedCrossHatch
};

Then hop into asset-inventory.cpp to wire up the mapping to the asset files - noting the new case statements for Torus and RedCrossHatch:

std::string ast::assets::resolveStaticMeshPath(const ast::assets::StaticMesh& staticMesh)
{
    switch (staticMesh)
    {
        case ast::assets::StaticMesh::Crate:
            return "assets/models/crate.obj";
        case ast::assets::StaticMesh::Torus:
            return "assets/models/torus.obj";

    }
}

std::string ast::assets::resolveTexturePath(const ast::assets::Texture& texture)
{
    switch (texture)
    {
        case ast::assets::Texture::Crate:
            return "assets/textures/crate.png";
        case ast::assets::Texture::RedCrossHatch:
            return "assets/textures/red_cross_hatch.png";
    }
}

Save and close the asset-inventory files and jump into scene-main.cpp. Update the prepare function to firstly load the new assets - remembering that if we don’t do this we will get a crash when we try to render them:

void prepare(ast::AssetManager& assetManager)
{
    ...

    assetManager.loadStaticMeshes({StaticMesh::Crate, StaticMesh::Torus});
    assetManager.loadTextures({Texture::Crate, Texture::RedCrossHatch});

    ...

Then also in the prepare function remove the existing staticMeshes.push_back line and enter the following to add four mesh instances into our scene at varying positions and rotations:

staticMeshes.push_back(ast::StaticMeshInstance{
    StaticMesh::Crate,           // Mesh
    Texture::Crate,              // Texture
    glm::vec3{0.4f, 0.6f, 0.0f}, // Position
    glm::vec3{0.6f, 0.6f, 0.6f}, // Scale
    glm::vec3{0.0f, 0.4f, 0.9f}, // Rotation axis
    0.0f});                      // Initial rotation

staticMeshes.push_back(ast::StaticMeshInstance{
    StaticMesh::Torus,            // Mesh
    Texture::RedCrossHatch,       // Texture
    glm::vec3{-0.6f, 0.4f, 0.0f}, // Position
    glm::vec3{0.4f, 0.4f, 0.4f},  // Scale
    glm::vec3{0.2f, 1.0f, 0.4f},  // Rotation axis
    0.0f});                       // Initial rotation

staticMeshes.push_back(ast::StaticMeshInstance{
    StaticMesh::Crate,             // Mesh
    Texture::Crate,                // Texture
    glm::vec3{-0.5f, -0.5f, 0.0f}, // Position
    glm::vec3{0.7f, 0.3f, 0.3f},   // Scale
    glm::vec3{0.2f, 0.6f, 0.1f},   // Rotation axis
    90.0f});                       // Initial rotation

staticMeshes.push_back(ast::StaticMeshInstance{
    StaticMesh::Torus,            // Mesh
    Texture::RedCrossHatch,       // Texture
    glm::vec3{0.6f, -0.4f, 0.0f}, // Position
    glm::vec3{0.4f, 0.4f, 0.4f},  // Scale
    glm::vec3{0.6f, 0.3f, 0.1f},  // Rotation axis
    50.0f});

Run the application again and you’ll now see some sweet torus models (yeah I actually just clicked Create Torus in my 3D program then exported it…) and two crates - one of which is scaled a bit:


Testing on all platforms

Run the application on each of our target platforms to observe the same output once again:

Emscripten

This is shown just above as the live demo.

Mac Console

MacOS

iOS

Android

Windows


Summary

That pretty much wraps up the core OpenGL side of this series - next we will delve into the Vulkan implementation for our project.

The code for this article can be found here.

Continue to Part 13: Vulkan introduction.

End of part 12