Shun the IDE, OpenGL with Make for me!

For long I’ve been wanting to get into OpenGL, hopefully this time being able to understand what is going on. I last looked into Java and LWJGL back when minecraft was peaking. It seems millennia has passed since then.

So, my current situation, techwise:

I’m running OS X (well, macOS, but the unnecessary new name can go to Redmond), and as an avid user of BASH and Emacs, IDEs give me the creeps. I prefer languages where a “test program” can be written in one, maximum two files. I want to be able to compile my program by hand, especially if it’s only one or two files. By hand I of course mean through my shell, using make.
Surely the cascading hellscapes of modern IDEs, with all their fancy buttons, web news feeds, package managers and Microsofty ways to fuck up git integration is unnecessary. At least if I’m only trying to do something simple and stupid, say, like using my graphics card for drawing a triangle. Right? Riiight?

So far, no luck.

OpenGL is really a jungle (to a newcomer). Compatibility mode, core profile, fixed funciton vs. programmable pipeline. It seems to be an endless list of techno mumbo jumbo. (It’s not, the documentation is just not easily accessible to humans).

I’ve tried multiple things to get some graphics programming going. I’ve tried Java and LWJGL as well as F# and OpenTK. I had a brief fling with MonoGame, the now open sourced game framework Microsoft called XNA, all the way back from the Xbox 360 days. MonoGame is awesome, and allows you to write multiplatform games using OpenGL, without using a “game editor” like Godot, Unity, GameMaker, UE4 or what have you. Not that those are bad, quite the opposite. But I want to learn OpenGL, not learn FancyAllPurpose Engine #5.

So, why not MonoGame?
First of all, MonoGame handles your graphics for you. You don’t need to write a texture-class, it’s already there. No fun in that! Can we at least write shaders? That’s a big part of OpenGL, and with the core profile, that is OpenGL >= 3.3 and the programmable pipeline , shaders are necessary to do anything. That’s simply how you draw things in OpenGL, as the old drawing calls are deprecated.

Well, you can’t write your own shader (okay okay, you can’t compile them locally). Not on a mac as least. As far as I understand, MonoGame comes with a Content Pipeline tool, meant to organise your assets, like textures, sounds, and fonts for you. This tool also helps compile your shaders, but for some reason, it only compiles HLSL, the directx shader language, and not GLSL. Apparently OpenGL is used afterwards, and this part really has me puzzled. But, to use custom shaders in MonoGame on macOS and GNU/Linux, you apparently need to set up an Azure account, and use a Windows server as a webservice to compile your shaders.

So, that seems stupid. And, to run monogame, we need an IDE, and what do we say to IDEs?
Not today satan. Not today.

Next semester I’ll be taking a course called Introduction to Computer Graphics, that as far as I know will use C++ and OpenGL for programming assignments. Neat! I haven’t touched C++ since 15 year old me “modded Half-Life 2”, by copy pasting wildly from the internet and changing stuff until it broke. I remember making a 3rd person camera replacing Freemans hands with Alyx’ entire model and moving the camera back (hoooow did I do that?) and even better: Changing the crossbow from firing bolts to exploding seagulls. Good times. I also remember not understanding C++ and having no clue what I was doing most of the time. 13 years later, maybe it’s time to stop being scared of the C++ beast.

Language chosen, we’re going C++! OpenGL is easy to use from C++, so this is good. No external libraries to supply bindings for us, like in Python or .NET.

There is a quite thorough tutorial on OpenGL, located at www.learnopengl.com. (It even goes beyond most tutorials, and ends by creating a breakout clone with OpenGL. There goes my summer I guess.)

Following the tutorial, I get to page 3. I’m on macOS, so OpenGL and its headers are already installed. Apparently I need something called GLFW.

What is GLFW? It’s a windowing framework. OpenGL needs something to draw on (a context to draw in?), and GLFW supplies that for us.

The tutorial then proceeds to talk about using CMake and Visual Studio to build GLFW from source, and linking the resulting library. Using Visual Studio seems like a waste of good mouse buttons when I already have a perfectly functioning keyboard that types commands. CMake “obscures” the make process, and my goal is to understand what is going on and feel just a little in control. Let’s open the helmet.

On macOS, there’s no need to build GLFW from source yourself. The brew package manager has GLFW in it’s repos, and it even has a build from source command.


brew install –build-from-source glfw

The tutorial goes on about linking on Windows and linking on Linux, and leaves the latte segment with shiny macs in the ditch. For now, just shrug your shoulders and sip you latte. We’ll get to the build/link part soon enough. Inspired by functional programming, we will evaluate this very lazily.

Now we need GLAD. What is GLAD and why do we need more stuff to clutter our code?

As I’ve said once or thrice, OpenGL is a jungle for the newcomer. GLAD tries to clearcut this jungle like a good-old logging company.
You go to a web service, select parameters for the OpenGL context you want, and get 3 files back. We will include these files in our project, and they will do a lot of boiler-plate stuff for us. This might seem contradictory to my whole “but I want to learn it all” idea, but let’s just start by drawing a triangle. Then we can rewrite boilerplate later.

GLAD is located here

We now have 3 files, GLFW installed and still neither a OpenGl C++ program, nor a way to build all this.

Let’s introduce a simple C++ OpenGL example, almost straight from www.learnopengl.com. If we try to compile that, maybe gcc will be nice enough to guide us in the right direction afterwards.

#include "glad.h"
#include 

#include 

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

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(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

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

    // render loop
    while (!glfwWindowShouldClose(window))
    {
        // input
        processInput(window);

        // render
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

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

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
void framebuffer_size_callback(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);
}

I will not detail what this piece of code does. If you want a thorough explanation, go through the Learn OpenGL tutorials.

My directory structure for the project currently looks like this:

  • glad.c
  • glad.h
  • khrplatform.h
  • main.cpp

I have manually updated the includes of GLAD and khrplatform.h in the files where this is relevant.
Change lines
#include to #include “glad.h” etc.

Now, all we need is a Makefile!

Instead of trying to write a Makefile from scratch, I borrow one from

lazyfoo.net

Lazyfoo also has a nice series on OpenGL, focusing on 2d instead of 3d. Lazyfoo uses compatibilyty mode/immediate mode/OpenGL=2.1, and does not switch to the programmable pipeline before tutorial 33. I like the reasoning behind, but I want to go straight to the programmable pipeline/core profile. The Makefile supplied by lazyfoo however, is a nice starting place for me. I modified it to suit my simple project. First I’ll show you the file, then we’ll go through it.

#OBJS specifies which files to compile as part of the project
OBJS = glad.c main.cpp

#CC specifies which compiler we're using
CC = g++

#INCLUDE_PATHS specifies the additional include paths we'll need
INCLUDE_PATHS = -I/usr/local/include -I/opt/X11/include

#LIBRARY_PATHS specifies the additional library paths we'll need
LIBRARY_PATHS = -L/usr/local/lib -I/opt/X11/lib -L/usr/local/Cellar/glfw/3.2.1/lib -lglfw

#COMPILER_FLAGS specifies the additional compilation options we're using
# -w suppresses all warnings
COMPILER_FLAGS = -w

#LINKER_FLAGS specifies the libraries we're linking against
LINKER_FLAGS = -framework OpenGL

#OBJ_NAME specifies the name of our exectuable
OBJ_NAME = learningGround

#This is the target that compiles our executable
all : $(OBJS)
	$(CC) $(OBJS) $(INCLUDE_PATHS) $(LIBRARY_PATHS) $(COMPILER_FLAGS) $(LINKER_FLAGS) -o $(OBJ_NAME)

clean : 
	rm -v learningGround

Most of this is pretty standard make. The “hard parts” are all the libraries we need to include.

First of all, main.cpp needs GLAD, so let’s compile glad.c before main.cpp.
I set the compiler to g++. My g++ is symlinked to the clang C++ compiler. Trouble may arise if you use GNU GCC, and some required libraries have been built with Clang. So, for ease of life, we stick to Clang.

Next, the include paths and lib paths.
Most of it is straight from the lazyfoo Makefile. However, lazyfoo uses freeGLUT to accomplish what we accomplish with GLFW.

To the library path we need to add GLFW. I know it’s installed through brew, but how the heck do I figure out what to write in the Makefile?

Introducing the wonderful tool, pkg-config. It’s also available in brew.

First, I check what version of GLFW I built earlier.


brew list –versions | grep glfw

My output is

glfw 3.2.1

Okay. It’s probably either glfw or glfw3 I’m looking for then.

I fire up pkg-config with both as input.

> pkg-config --libs glfw
Package glfw was not found in the pkg-config search path.
Perhaps you should add the directory containing `glfw.pc'
to the PKG_CONFIG_PATH environment variable
No package 'glfw' found
> pkg-config --libs glfw3
-L/usr/local/Cellar/glfw/3.2.1/lib -lglfw

Awesome, glfw3 worked. That goes in the makefile, under LIBRARY_PATHS

The compiler is then set to suppress all warnings. This is most probably stupid, but right now I’m only concerned with getting something built, not making the compiler happy.

Under LINKER_FLAGS, we need to tell the compiler we’re using a framework called OpenGL. If we do not, the compiler won’t be able to find OpenGL and all our efforts are for nothing!

LINKER_FLAGS = -framework OpenGL

We set OBJ_NAME, the name of the resulting binary after running make.

Then we have a target, all. It takes OBJS as input (meaning glad.c and main.cpp), then mashes together a string we can execute, injecting first our compiler choice (CC), our files (OBJS), our include paths, library paths, compiler flags, linker flags and finally the name of the resulting binary.
For good measure, I add a clean target, so we can delete the binary by writing


make clean

So far so good. We can try to run make, and hopefully we should now get a window (from GLFW) that has an OpenGL Context!

I thought about putting a screenshot here but decided not to. Just imagine 480.000dark green pixels in a 800×600 rectangular window. What a beauty!

That was a lot of effort and we still haven’t drawn anything but a greenish background color. I am sure I forgot a step somewhere, or that I already had something obscure installed on my system, that you will be missing if you follow this “guide”.

We have so far accomplished my goal. I have a way to create OpenGL projects in C++ and build them with make. I won’t have to click on anything before I start writing code that handles input in my game. That’s the dream, isn’t it?