Skip to content

Rendering Sprites

Math Lib

GLM

GLM is a header only math library based on the OpenGL Shading Language (GLSL) specifications. Most things related to math such as vectors, matrices, etc... will be handled by GLM.

Rectangle

The Rectangle class will mostly be used for defining the bounds of a sprite's draw source and draw destination which we'll get to later.

#pragma once

#include <iostream>

#include <glm/glm.hpp>

class Rectangle {
  public:
    float x;
    float y;
    float w;
    float h;

    Rectangle(): x(0.0f), y(0.0f), w(0.0f), h(0.0f) {}
    Rectangle(float x, float y, float w, float h): x(x), y(y), w(w), h(h) {}
    Rectangle(float x, float y, glm::vec2 size): x(x), y(y), w(size.x), h(size.y) {}
    Rectangle(glm::vec2 position, glm::vec2 size): x(position.x), y(position.y), w(size.x), h(size.y) {}

    bool operator==(const Rectangle &otherRect2) const {
        return this->x == otherRect2.x && this->y == otherRect2.y && this->w == otherRect2.w && this->h == otherRect2.h;
    }

    bool operator!=(const Rectangle &otherRect2) const {
        return !(*this == otherRect2);
    }

    friend std::ostream& operator<<(std::ostream& os, const Rectangle &r);
};
#include "rectangle.h"

std::ostream& operator<<(std::ostream& os, const Rectangle &r) {
    os << "(" << r.x << ", " << r.y << ", " << r.w << ", " << r.h << ")";
    return os;
}

Custom Math Header

Red Engine will use GLM, but we may want to use another math library or write our own in the future. To make this easier, will we define type aliases for math related classes.

#pragma once

#include "rectangle.h"

using Vector2 = glm::vec2;
using IVector2 = glm::ivec2;
using Vector3 = glm::vec3;
using Matrix4 = glm::mat4;
using Rect2 = Rectangle;

Color

Not much to explain other than needing a concept of color within the engine.

#pragma once

#include <glad/glad.h>

class Color {
  public:
    float r = 1.0f;
    float g = 1.0f;
    float b = 1.0f;
    float a = 1.0f;

    Color() {}
    Color(GLfloat red, GLfloat green, GLfloat blue) : r(red), g(green), b(blue), a(1.0f) {}
    Color(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) : r(red), g(green), b(blue), a(alpha) {}

    Color operator*(float value) {
        return Color(this->r * value, this->g * value, this->b * value, this->a);
    }
};

Renderer Batcher

The RendererBatcher class will be used to take draw calls and batch them to be used during rendering. Typically, batching is done to limit draw calls for efficiency and performance reasons but for simplicity’s sake this batcher will be designed to just defer and submit draw calls during the Render phase.

#pragma once

#include <vector>
#include <map>
#include <string>
#include <functional>

#include "./re/rendering/color.h"
#include "./re/rendering/texture.h"
#include "./re/math/math.h"

struct SpriteBatchItem {
    Texture *texture2D = nullptr;
    Rect2 sourceRectangle;
    Rect2 destinationRectangle;
    float rotation = 0.0f;
    Color color = Color(1.0f, 1.0f, 1.0f, 1.0f);
    bool flipX = false;
    bool flipY = false;
};

struct ZIndexDrawBatch {
    std::vector<SpriteBatchItem> spriteDrawBatches;
};

using RenderFlushFunction = std::function<void(const int zIndex, const ZIndexDrawBatch &zIndexDrawBatch)>;

class RendererBatcher {
  public:
    void BatchDrawSprite(SpriteBatchItem spriteBatchItem, int zIndex);
    void Flush(const RenderFlushFunction &renderFlushFunction);

  private:
    std::map<int, ZIndexDrawBatch> drawBatches;
};
#include "./renderer_batcher.h"

void RendererBatcher::BatchDrawSprite(SpriteBatchItem spriteBatchItem, int zIndex) {
    if (drawBatches.find(zIndex) == drawBatches.end()) {
        drawBatches.emplace(zIndex, ZIndexDrawBatch{});
    }
    drawBatches[zIndex].spriteDrawBatches.emplace_back(spriteBatchItem);
}

void RendererBatcher::Flush(const RenderFlushFunction &renderFlushFunction) {
    for (const auto &pair : drawBatches) {
        const int zIndex = pair.first;
        const ZIndexDrawBatch &zIndexDrawBatch = pair.second;
        renderFlushFunction(zIndex, zIndexDrawBatch);
    }
    drawBatches.clear();
}

The first thing to point out is the SpriteBatchItem struct as this will be used to determine what to render for a sprite. Red Engine will group draw batches by their z indices. The ZIndexDrawBatch struct will represent a draw batch for a particular z index. We also define an alias RenderFlushFunction to be used as a lambda function to render flushed draw batches by the Renderer which we'll get to later.

RendererBatcher class is currently simple as it has one map and two functions. The drawBatches map maintains the order of draw batches based on their z indices. BatchDrawSprite will submit a batch item to the batcher to draw during the render phase. The Flush function is called during the render phase to render batch items submitted and will clear them when completed.

2D Rendering

Shaders

What are shaders? They are simply GPU programs, in the case of OpenGL written in GLSL (OpenGL Shading Language). Before we write the renderer for Red Engine we'll first implement the Shader class which will compile shaders, install the shaders for use, and set uniform variables.

A shader can be written for each of the shader stages, but we will only use vertex and fragment shaders in Red Engine.

#pragma once

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

#include <glad/glad.h>

#include "../math/math.h"
#include "./color.h"
#include "../utils/logger.h"

struct OpenGLShaderSourceCode {
    std::string vertex;
    std::string fragment;
};

class Shader {
  public:
    Shader();
    Shader(const std::string &vertexPath, const std::string &fragmentPath);
    Shader(OpenGLShaderSourceCode openGlShaderSourceCode);
    ~Shader();
    OpenGLShaderSourceCode GetOpenGLShaderSourceFromPaths(const std::string &vertexPath, const std::string &fragmentPath);
    void Compile(OpenGLShaderSourceCode openGlShaderSourceCode);
    void Use();
    void SetBool(const std::string &name, bool value) const;
    void SetInt(const std::string &name, int value) const;
    void SetFloat(const std::string &name, float value) const;
    void SetVec2Float(const std::string &name, float v1, float v2) const;
    void SetVec2Float(const std::string &name, const Vector2 &value) const;
    void SetVec3Float(const std::string &name, const Color &value) const;
    void SetVec3Float(const std::string &name, const Vector3 &value) const;
    void SetVec3Float(const std::string &name, float v1, float v2, float v3) const;
    void SetVec4Float(const std::string &name, float v1, float v2, float v3, float v4) const;
    void SetVec4Float(const std::string &name, const Color &value) const;
    void SetMatrix4Float(const std::string &name, const Matrix4 &mat) const;

  private:
    unsigned int ID;
    Logger *logger = nullptr;

    bool IsShaderFilesValid(const std::string &vertexPath, const std::string &fragmentPath);
    void CheckCompileErrors(unsigned int shader, const std::string &type);
};
#include "shader.h"

#include <cassert>

#include <glm/gtc/type_ptr.hpp>

#include "../utils/file_helper.h"

Shader::Shader() : logger(Logger::GetInstance()) {}

Shader::Shader(const std::string &vertexPath, const std::string &fragmentPath) : logger(Logger::GetInstance()) {
    Compile(GetOpenGLShaderSourceFromPaths(vertexPath, fragmentPath));
}

Shader::Shader(OpenGLShaderSourceCode openGlShaderSourceCode) : logger(Logger::GetInstance()) {
    Compile(openGlShaderSourceCode);
}

Shader::~Shader() {}

OpenGLShaderSourceCode Shader::GetOpenGLShaderSourceFromPaths(const std::string &vertexPath, const std::string &fragmentPath) {
    OpenGLShaderSourceCode openGlShaderSourceCode;
    if (IsShaderFilesValid(vertexPath, fragmentPath)) {
        std::ifstream vertexShaderFile;
        std::ifstream fragmentShaderFile;
        vertexShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        fragmentShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        try {
            vertexShaderFile.open(vertexPath.c_str());
            fragmentShaderFile.open(fragmentPath.c_str());
            std::stringstream vertexShaderStream, fragmentShaderStream;
            vertexShaderStream << vertexShaderFile.rdbuf();
            fragmentShaderStream << fragmentShaderFile.rdbuf();
            vertexShaderFile.close();
            fragmentShaderFile.close();
            openGlShaderSourceCode.vertex = vertexShaderStream.str();
            openGlShaderSourceCode.fragment = fragmentShaderStream.str();
        } catch(std::ifstream::failure& e) {
            logger->Error("Error reading shader files!\n"
                          "vertex path = '%s'\nfragment path = '%s'!", vertexPath.c_str(), fragmentPath.c_str());
        }
    }
    return openGlShaderSourceCode;
}

void Shader::Compile(OpenGLShaderSourceCode openGlShaderSourceCode) {
    unsigned int vertex, fragment;
    const char *vertexSource = openGlShaderSourceCode.vertex.c_str();
    const char *fragmentSource = openGlShaderSourceCode.fragment.c_str();
    // vertex
    vertex = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex, 1, &vertexSource, nullptr);
    glCompileShader(vertex);
    CheckCompileErrors(vertex, "VERTEX");
    // fragment
    fragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment, 1, &fragmentSource, nullptr);
    glCompileShader(fragment);
    CheckCompileErrors(fragment, "FRAGMENT");
    // shader
    ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    CheckCompileErrors(ID, "PROGRAM");
    glDeleteShader(vertex);
    glDeleteShader(fragment);
}

void Shader::Use() {
    glUseProgram(ID);
}

void Shader::SetBool(const std::string &name, bool value) const {
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}

void Shader::SetInt(const std::string &name, int value) const {
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::SetFloat(const std::string &name, float value) const {
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::SetVec2Float(const std::string &name, float v1, float v2) const {
    glUniform2f(glGetUniformLocation(this->ID, name.c_str()), v1, v2);
}

void Shader::SetVec2Float(const std::string &name, const Vector2 &value) const {
    glUniform2f(glGetUniformLocation(this->ID, name.c_str()), value.x, value.y);
}

void Shader::SetVec3Float(const std::string &name, float v1, float v2, float v3) const {
    glUniform3f(glGetUniformLocation(this->ID, name.c_str()), v1, v2, v3);
}

void Shader::SetVec3Float(const std::string &name, const Color &value) const {
    glUniform3f(glGetUniformLocation(this->ID, name.c_str()), value.r, value.g, value.b);
}

void Shader::SetVec3Float(const std::string &name, const Vector3 &value) const {
    glUniform3f(glGetUniformLocation(this->ID, name.c_str()), value.x, value.y, value.z);
}

void Shader::SetVec4Float(const std::string &name, float v1, float v2, float v3, float v4) const {
    glUniform4f(glGetUniformLocation(this->ID, name.c_str()), v1, v2, v3, v4);
}

void Shader::SetVec4Float(const std::string &name, const Color &value) const {
    glUniform4f(glGetUniformLocation(this->ID, name.c_str()), value.r, value.g, value.b, value.a);
}

void Shader::SetMatrix4Float(const std::string &name, const Matrix4 &mat) const {
    glUniformMatrix4fv(glGetUniformLocation(this->ID, name.c_str()), 1, GL_FALSE, glm::value_ptr(mat));
}

bool Shader::IsShaderFilesValid(const std::string &vertexPath, const std::string &fragmentPath) {
    bool isValid = true;
    if (!FileHelper::DoesFileExist(vertexPath)) {
        isValid = false;
        logger->Error("Vertex file: %s doesn't exist!", vertexPath.c_str());
    }
    if (!FileHelper::DoesFileExist(fragmentPath)) {
        isValid = false;
        logger->Error("Vertex file: %s doesn't exist!", vertexPath.c_str());
    }
    return isValid;
}

void Shader::CheckCompileErrors(unsigned int shader, const std::string &type) {
    int success;
    char infoLog[1024];
    if(type == "PROGRAM") {
        glGetProgramiv(shader, GL_LINK_STATUS, &success);
        if(!success) {
            glGetProgramInfoLog(shader, 1024, nullptr, infoLog);
            logger->Error("Shader type '%s' linking failed!\n%s", type.c_str(), infoLog);
        }
    } else {
        glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
        if(!success) {
            glGetShaderInfoLog(shader, 1024, nullptr, infoLog);
            logger->Error("Shader type '%s' compilation failed!\n%s", type.c_str(), infoLog);
        }
    }
}

First, the OpenGLShaderSourceCode struct is used to hold a string representation of the shader code. The Shader class has two constructors, one that accepts OpenGLShaderSourceCode and another that accepts the file paths of the vertex and fragment shaders file paths as parameters which we won't use for this tutorial series.

The Compile function compiles the vertex and fragment shaders, attaches the shader objects to the shader program, and links the shader program. We will call the Use function whenever we need to install a shader for use which we'll see an example of later. Other than that, the last thing to mention is the multiple Set$TYPE functions. These are used to set uniform variables within the shaders and we will see how they are used in the next section.

SpriteRenderer

Shader Code

The first thing to point out is the GLSL shader code for the vertex and fragment shaders.

Vertex

The vec4 variable vertex is a vertex attribute that contains data for the sprites position and texture coordinates. Next is the out variable for texCoord which we'll read from our vertex attribute and pass to the fragment shader. Next are the two uniform variables for the projection and model matrices. The main function simply set's the value for texCoord and set's the position of the vertex via gl_Position.

#version 330 core

layout (location = 0) in vec4 vertex;

out vec2 texCoord

uniform mat4 projection;
uniform mat4 model;

void main() {
    texCoord = vertex.zw;
    gl_Position = projection * model * vec4(vertex.xy, 0.0f, 1.0f);
}

Fragment

The fragment shader is even simpler than the vertex as we're only setting the color of the fragment! We get the color of the texture with the texture function. We then take the color from the texture and blend it with the spriteColor passed in as a uniform variable.

#version 330 core

in vec2 texCoord;
out vec4 color;

uniform sampler2D sprite;
uniform vec4 spriteColor;

void main() {
    color = spriteColor * texture(sprite, texCoord);
}

SpriteRenderer Class

The next thing to tackle it to write the renderer class for sprites!

#pragma once

#include <glad/glad.h>

#include "./re/rendering/shader.h"
#include "./re/rendering/texture.h"
#include "./re/rendering/color.h"
#include "./re/math/math.h"

#include "project_properties.h"

static const std::string &OPENGL_SHADER_SOURCE_VERTEX_SPRITE =
        "#version 330 core\n"
        "\n"
        "layout (location = 0) in vec4 vertex;\n"
        "\n"
        "out vec2 texCoord;\n"
        "\n"
        "uniform mat4 projection;\n"
        "uniform mat4 model;\n"
        "\n"
        "void main() {\n"
        "    texCoord = vertex.zw;\n"
        "    gl_Position = projection * model * vec4(vertex.xy, 0.0f, 1.0f);\n"
        "}\n"
        "";

static const std::string &OPENGL_SHADER_SOURCE_FRAGMENT_SPRITE =
        "#version 330 core\n"
        "\n"
        "in vec2 texCoord;\n"
        "out vec4 color;\n"
        "\n"
        "uniform sampler2D sprite;\n"
        "uniform vec4 spriteColor;\n"
        "\n"
        "void main() {\n"
        "    color = spriteColor * texture(sprite, texCoord);\n"
        "}\n"
        "";

static const OpenGLShaderSourceCode OPENGL_SHADER_SOURCE_SPRITE = OpenGLShaderSourceCode{
    .vertex = OPENGL_SHADER_SOURCE_VERTEX_SPRITE,
    .fragment = OPENGL_SHADER_SOURCE_FRAGMENT_SPRITE
};

class SpriteRenderer {
  public:
    SpriteRenderer();
    void Draw(Texture *texture2D, const Rect2 &sourceRectangle, const Rect2 &destinationRectangle, float rotation,
            const Color &color, bool flipX, bool flipY);

  private:
    Shader shader;
    GLuint quadVAO;
    GLuint quadVBO;
    ProjectProperties *projectProperties = nullptr;
};
#include "sprite_renderer.h"

#include <glm/gtc/matrix_transform.hpp>

SpriteRenderer::SpriteRenderer() : projectProperties(ProjectProperties::GetInstance()) {
    GLfloat vertices[] = {
        // positions // texture coordinates
        0.0f, 1.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 0.0f,

        0.0f, 1.0f, 0.0f, 1.0f,
        1.0f, 1.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 0.0f
    };

    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);

    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindVertexArray(quadVAO);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*) nullptr);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    shader = Shader(OPENGL_SHADER_SOURCE_SPRITE);
    shader.Use();
    shader.SetInt("sprite", 0);
    Matrix4 projection = glm::ortho(0.0f, static_cast<float>(projectProperties->GetWindowWidth()), static_cast<float>(projectProperties->GetWindowHeight()), 0.0f, -1.0f, 1.0f);
    shader.SetMatrix4Float("projection", projection);
}

void
SpriteRenderer::Draw(Texture *texture2D, const Rect2 &sourceRectangle, const Rect2 &destinationRectangle, float rotation, const Color &color,
                     bool flipX, bool flipY) {
    // 1. Translation
    Matrix4 model = glm::mat4(1.0f);
    model = glm::translate(model, glm::vec3(destinationRectangle.x, destinationRectangle.y, 0.0f));  // first translate (transformations are: scale happens first, then rotation, and then final translation happens; reversed order)
    // 2. Rotation
    model = glm::translate(model, Vector3(0.5f * destinationRectangle.w, 0.5f * destinationRectangle.h, 0.0f)); // move origin of rotation to center of quad
    model = glm::rotate(model, glm::radians(rotation), Vector3(0.0f, 0.0f, 1.0f)); // then rotate
    model = glm::translate(model, Vector3(-0.5f * destinationRectangle.w, -0.5f * destinationRectangle.h, 0.0f)); // move origin back
    // 3. Scaling
    model = glm::scale(model, Vector3(destinationRectangle.w, destinationRectangle.h, 1.0f)); // last scale

    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);

    shader.Use();
    shader.SetMatrix4Float("model", model);
    shader.SetVec4Float("spriteColor", color.r, color.g, color.b, color.a);

    glActiveTexture(GL_TEXTURE0);
    texture2D->Bind();

    // render subimage based on source rectangle
    glPixelStorei(GL_UNPACK_ROW_LENGTH, texture2D->GetWidth());
    glPixelStorei(GL_UNPACK_SKIP_PIXELS, sourceRectangle.x);
    glPixelStorei(GL_UNPACK_SKIP_ROWS, sourceRectangle.y);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sourceRectangle.w, sourceRectangle.h, 0, texture2D->GetImageFormat(), GL_UNSIGNED_BYTE, texture2D->GetData());

    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
    glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);

    // Render Container
    GLfloat vertices[6][4] = {
            // positions // texture coordinates
            0.0f, 1.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 0.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            1.0f, 1.0f, 1.0f, 1.0f,
            1.0f, 0.0f, 1.0f, 0.0f
    };

    static const int VERT_X = 2;
    static const int VERT_Y = 3;
    if (flipX) {
        vertices[0][VERT_X] = 1.0f;
        vertices[1][VERT_X] = 0.0f;
        vertices[2][VERT_X] = 1.0f;
        vertices[3][VERT_X] = 1.0f;
        vertices[4][VERT_X] = 0.0f;
        vertices[5][VERT_X] = 0.0f;
    }

    if (flipY) {
        vertices[0][VERT_Y] = 0.0f;
        vertices[1][VERT_Y] = 1.0f;
        vertices[2][VERT_Y] = 1.0f;
        vertices[3][VERT_Y] = 0.0f;
        vertices[4][VERT_Y] = 0.0f;
        vertices[5][VERT_Y] = 1.0f;
    }

    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
    glDrawArrays(GL_TRIANGLES, 0, 6);

    glBindVertexArray(0);
}

Will explain soon...

Renderer2D

We have a SpriteRenderer and now it's time to define the top level 2d renderer class.

#pragma once

#include "renderer_batcher.h"
#include "sprite_renderer.h"

class Renderer2D {
  public:
    Renderer2D() = default;
    ~Renderer2D();
    void Initialize();
    void SubmitSpriteBatchItem(Texture *texture2D, Rect2 sourceRectangle, Rect2 destinationRectangle, int zIndex, float rotation = 0.0f, Color color = Color(1.0f, 1.0f, 1.0f), bool flipX = false, bool flipY = false);
    void FlushBatches();

  private:
    RendererBatcher rendererBatcher;
    SpriteRenderer *spriteRenderer = nullptr;
};
#include "renderer_2d.h"

#include <cassert>
#include <glad/glad.h>

Renderer2D::~Renderer2D() {
    if (spriteRenderer != nullptr) {
        delete spriteRenderer;
    }
}

void Renderer2D::Initialize() {
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    spriteRenderer = new SpriteRenderer();
}


void Renderer2D::SubmitSpriteBatchItem(Texture *texture2D, Rect2 sourceRectangle, Rect2 destinationRectangle,
                                       int zIndex, float rotation,
                                       Color color, bool flipX, bool flipY) {
    SpriteBatchItem spriteBatchItem = {
        texture2D,
        sourceRectangle,
        destinationRectangle,
        rotation,
        color,
        flipX,
        flipY
    };
    rendererBatcher.BatchDrawSprite(spriteBatchItem, zIndex);
}

void Renderer2D::FlushBatches() {
    assert(spriteRenderer != nullptr && "SpriteRenderer is NULL, initialize the Renderer2D before using!");

    const RenderFlushFunction &renderFlushFunction = [this] (const int zIndex, const ZIndexDrawBatch &zIndexDrawBatch) {
        for (const SpriteBatchItem &spriteBatchItem : zIndexDrawBatch.spriteDrawBatches) {
            spriteRenderer->Draw(spriteBatchItem.texture2D,
                                 spriteBatchItem.sourceRectangle,
                                 spriteBatchItem.destinationRectangle,
                                 spriteBatchItem.rotation,
                                 spriteBatchItem.color,
                                 spriteBatchItem.flipX,
                                 spriteBatchItem.flipY);
        }
    };
    rendererBatcher.Flush(renderFlushFunction);
}

The Initialize function is straightforward as it initialized things for the renderer. SubmitSpriteBatchItem function will submit a sprite batch item to be rendered at a later time. Last but not least the FlushBatches will flush all queued draw batches and render them to the screen.

Render Sprites

Updating constructor to initialize member pointer variables.

GameEngine::GameEngine() :
    projectProperties(ProjectProperties::GetInstance()),
    engineContext(GameEngineContext::GetInstance()),
    renderContext(RenderContext::GetInstance()),
    assetManager(AssetManager::GetInstance()),
    fpsCounter(FPSCounter::GetInstance()),
    logger(Logger::GetInstance()) {
    Initialize();
}

Implementing the private InitializeRendering function.

bool GameEngine::InitializeRendering() {
    // OpenGL attributes
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

    renderContext->window = SDL_CreateWindow(
                                projectProperties->GetGameTitle().c_str(),
                                SDL_WINDOWPOS_CENTERED,
                                SDL_WINDOWPOS_CENTERED,
                                projectProperties->GetWindowWidth(),
                                projectProperties->GetWindowHeight(),
                                renderContext->windowFlags);
    renderContext->glContext = SDL_GL_CreateContext(renderContext->window);
    renderContext->currentWindowWidth = projectProperties->GetWindowWidth();
    renderContext->currentWindowHeight = projectProperties->GetWindowHeight();

    if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) {
        logger->Error("Couldn't initialize glad");
        return false;
    }

    renderer2D.Initialize();

    // Temp Load Assets
    assetManager->LoadTexture("assets/images/melissa_walk_animation.png", "assets/images/melissa_walk_animation.png");
    return true;
}

Now we will implement the Render function.

void GameEngine::Render() {
    glClearColor(projectProperties->backgroundClearColor.r,
                 projectProperties->backgroundClearColor.g,
                 projectProperties->backgroundClearColor.b,
                 projectProperties->backgroundClearColor.a);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    // Render Sprites
    static Texture *mellisaWalkTexture = assetManager->GetTexture("assets/images/melissa_walk_animation.png");
    static Rect2 drawSourceRect = Rect2(0, 0, 32, 32);
    static Rect2 drawDestinationRect = Rect2(windowCenter.x, windowCenter.y, drawSourceRect.w, drawSourceRect.h);
    renderer2D.SubmitSpriteBatchItem(mellisaWalkTexture, drawSourceRect, drawDestinationRect, 0);

    // Flush
    renderer2D.FlushBatches();


    SDL_GL_SwapWindow(renderContext->window);
}

Lastly we are going to process input by checking if an SDL_QUIT event type was triggered. If we click the 'X' in the top right part of the window, we will now close the window and shutdown the engine.

void GameEngine::ProcessInput() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        switch(event.type) {
        case SDL_QUIT:
            engineContext->SetRunning(false);
            break;
        case SDL_WINDOWEVENT:
            switch (event.window.event) {
            case SDL_WINDOWEVENT_RESIZED:
                renderContext->currentWindowWidth = event.window.data1;
                renderContext->currentWindowHeight = event.window.data2;
                glViewport(0, 0, renderContext->currentWindowWidth, renderContext->currentWindowHeight);
                break;
            }
            break;
        }
    }
}

After executing the code, this will be rendered to the screen:

Rendering Sprite Screenshot

The source code for this section can be found here. Now that we have rendered a sprite it's now time to render font.