Introduction

Initial Setup

Before I even start drawing a need a window where I can place my pixels. For this project I have chosen GLFW which is a cross-platform library specially made for OpenGL and Vulkan development. It provides an API for creating windows, and allows me to also listen to input and events. I also need a library that loads all the OpenGL functions dynamically. For this I use GLAD.

Window Context

This will be my entry point of the engine and I’ll try to keep it as simple as possible. The main objective here is to create a window using GLFW and listen to input events, then I want to enter the “Render Loop” where I plan to call into the renderer to do all the draw calls needed. The main.cpp file looks like this:

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

#include "context.h"

void FramebufferSizeCallback(GLFWwindow *Window, int Width, int Height);
void MouseScrollCallback(GLFWwindow *Window, double OffsetX, double OffsetY);
void ProcessInput(context *Context);

// Settings
const unsigned int SCREEN_WIDTH = 1200;
const unsigned int SCREEN_HEIGHT = 800;

context Context;

int main() {
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // GLFW window creation
    // --------------------
    GLFWwindow *Window =
        glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "3D Engine", NULL, NULL);
    Context = {
        .Window = Window,
        .ScreenWidth = SCREEN_WIDTH,
        .ScreenHeight = SCREEN_HEIGHT,
        .FramebufferWidth = SCREEN_WIDTH,
        .FramebufferHeight = SCREEN_HEIGHT,
        .DeltaTime = 0.0f,
    };

    if (Context.Window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(Context.Window);
    glfwSetFramebufferSizeCallback(Context.Window, FramebufferSizeCallback);
    glfwSetScrollCallback(Context.Window, MouseScrollCallback);

    // GLAD: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    glfwGetFramebufferSize(Context.Window, &Context.FramebufferWidth,
                           &Context.FramebufferHeight);
    Context.ScreenWidth = Context.FramebufferWidth;
    Context.ScreenHeight = Context.FramebufferHeight;
    glViewport(0, 0, Context.FramebufferWidth, Context.FramebufferHeight);

    // Render Loop
    // -----------
    while (!glfwWindowShouldClose(Context.Window)) {
        // per-frame time logic
        // --------------------
        float CurrentFrame = static_cast<float>(glfwGetTime());
        Context.DeltaTime = CurrentFrame - Context.LastFrame;
        Context.LastFrame = CurrentFrame;

        // This keeps the framebuffer dimensions in sync with the window
        // dimensions
        glfwGetFramebufferSize(Context.Window, &Context.FramebufferWidth,
                               &Context.FramebufferHeight);
        if (Context.FramebufferWidth <= 0 || Context.FramebufferHeight <= 0) {
            glfwPollEvents();
            continue;
        }

        // Input
        // --------------------
        ProcessInput(&Context);

        // Render
        // ------
        // TODO: Here goes my draw calls


        // GLFW: swap buffers and poll IO events (keys pressed/released, mouse
        // moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(Context.Window);
        glfwPollEvents();
    }

    // GLFW: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// Input: query GLFW whether relevant keys are pressed/released this
// frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void ProcessInput(context *Context) {
    if (glfwGetKey(Context->Window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(Context->Window, true);
    }
}

// GLFW: whenever the window size changed (by OS or user resize) this callback
// function executes
// ---------------------------------------------------------------------------------------------
void FramebufferSizeCallback(GLFWwindow *Window, int Width, int Height) {
    // make sure the viewport matches the new window dimensions; note that width
    // and height will be significantly larger than specified on retina
    // displays.
    glViewport(0, 0, Width, Height);

    Context.ScreenWidth = Width;
    Context.ScreenHeight = Height;
}

void MouseScrollCallback(GLFWwindow *Window, double OffsetX, double OffsetY) {
    // TODO: Adjust camera zoom
}

I first initialize GLFW and told it what version of OpenGL I plan to use (3.3 in this case). I also registered some event handlers that I will use later to handle input events. Then I use GLAD to load all the definitions for the OpenGL functions.

Render Loop

Once I have everything initialized, I enter the “Render Loop”. The Render Loop is just an infinite loop that runs until the program is exited. The objective here is to process input events, advance the state of the renderer (updating any animation), draw objects to a framebuffer and finally swap the backbuffer (where we just draw) with the main window buffer.

Context struct

The context struct is a container for window related stuff plus some other data like the frame time and mouse state. I’m not super convinced about this struct yet as it is a bit random. But for now I will keep that data together to make is easy to access.

#ifndef CONTEXT_H_
#define CONTEXT_H_

#include <GLFW/glfw3.h>

struct context {
    GLFWwindow *Window;
    int ScreenWidth;
    int ScreenHeight;
    int FramebufferWidth;
    int FramebufferHeight;

    float DeltaTime;
    float LastFrame;

    float LastX;
    float LastY;
    bool FirstClick;
};

#endif

Compiling

Now I just need to compile and run the executable created. For this I have created a simple Makefile that allows me to run some make commands:

APP_NAME = 3DEngine
BUILD_DIR = ./bin
RESOURCES_DIR = resources
C_FILES = ./src/*.c ./src/*.cpp ./src/imgui/*.cpp
CFLAGS = -Wall -g -O0 -std=c++17

UNAME_S := $(shell uname -s)

ifeq ($(UNAME_S), Linux)
	INCLUDES = -I/usr/include -I/usr/local/include
	LDFLAGS = -L/usr/lib -L/usr/local/lib
	LIBS = -lglfw -lGL -ldl -lassimp -Wl,-rpath,/usr/local/lib
endif

ifeq ($(UNAME_S), Darwin)
	INCLUDES = -I/usr/include -I/usr/local/include
	LDFLAGS = -L/usr/lib -L/usr/local/lib
	LIBS = -lglfw -lassimp -framework GLUT -framework OpenGL -Wl,-rpath,/usr/local/lib
endif

all: build copy_resources

build:
	mkdir -p $(BUILD_DIR)
	clang++ $(CFLAGS) $(C_FILES) -o $(BUILD_DIR)/$(APP_NAME) $(INCLUDES) $(LDFLAGS) $(LIBS)

copy_resources:
	cp -r $(RESOURCES_DIR) $(BUILD_DIR)/

clean:
	rm -rf $(BUILD_DIR)

When I run the make command, the executable named 3DEngine is added to the ./bin folder. I can now just run in with ./bin/3DEngine and an empty window should appear.

Now that I have a window, I can start with the core topics of this engine project.

Squared Wave SVG