Introduction and Setting Up
Welcome to the first lesson in this tutorial series covering the basics of rendering graphics!
This lesson will be a quick one hopefully - we're going to look at how to setup and layout your workspace so that you can build and run the code for the rest of the lessons.
Libraries
Before we get into that though, I just want to take a minute to look at the libraries we will be using to build our program, and justify our choice over other similar tools.
OpenGL
In these tutorials, we will be using OpenGL, an open graphics library. OpenGL basically serves as a API to draw graphics to our screen in a way that can fully and automatically exploit the power and speed of GPUs. This API is provided by your graphics card driver, so should always be present on any up-to-date desktop computer, and means your code to draw something to the screen will run on pretty much any graphics card without alteration. Its widely known, supported, and kept up-to-date.
The two main alternatives to OpenGL are DirectX and Vulkan.
DirectX is locked to the Windows platform so cross-platform programs are not possible. Meanwhile, an OpenGL program can be compiled and run on Windows, Linux and Mac without any changes to the code!
Vulkan is a new graphics API released by the same community which organises OpenGL. While the two APIs perform similar roles, Vulkan is not a replacement for OpenGL. Vulkan provides really low-level raw access to the GPU, while OpenGL leaves a lot of the really low-level stuff up to the GPU driver to perform.
As a result Vulkan programs can be ultra-optimised and so slightly faster, but it is far more complex code-wise, and the speed advantage is unlikely to be worth the extra effort unless you really are chasing after every possible performance gain. For this reason, OpenGL still has it's place, as its much easier and faster to code, and far more beginner friendly. If you are new to graphics, this is the place to start. And if you do decide to switch to Vulkan in the future, many of the concepts learned for OpenGL will directly translate to Vulkan.
SDL
OpenGL offers us a graphics API, but it is primarily focused on rendering. It will allow us to draw fantastic images, but we still need to create a window on our desktop to display it in - something OpenGL doesn't handle as it's only concern is rendering. To handle cross-platform window functions, we'll also incorporate SDL, a library designed exactly for this purpose. It handles creating windows on our desktop, getting mouse and keyboard input, handling window resizing and much more. Importantly again it abstracts all of these functions so that your code doesn't need to change whether you are compiling it on Windows, Linux or Mac. It's a very widely used tool, and is what major developers such as Valve use.
There are various alternatives to SDL, for example GLFW and GLUT. Both are excellent libraries. However SDL offers slightly more functionality in terms of audio and controller support, and is what I personally have been using for many years, and for that reason is the library we will use in these tutorials.
NOTE: For these tutorials, I will be using SDL version 3. This is the latest version as of the time of writing (2024), but is currently still under development. The good news is that if you are following these lessons your program will be future-proof and this will be beneficial to your programs in the long run (there are some fantastic new features which encouraged me to target an as-of-yet unreleased library). In the short term though you may have some extra difficulty in setting the library up, at least until it is fully released, compared to version 2 which can simply be installed on most systems with a one-liner in the terminal.
Installation
To begin, we will of course need a C++ compiler installed, as well as the SDL and OpenGL libraries.
We can install the g++ compiler (GNU C++) from our terminal, as well as various other build tools, using the build-essential
package:
|
As OpenGL is technically just an API with an implementation provided by your graphics card driver, there is no need to install it. However, our program has no idea which functions exist in the driver - especially since OpenGL functions evolve over time with new versions, accommodating new features on graphics cards.
Therefore we'll use a library called GLEW, or the OpenGL Extension Wrangler. This library handles connecting our OpenGL calls to those provided by the GPU driver at run-time, and provides header files that we can include in our project describing all the functions which exist for the version of OpenGL which we target. It can be install like this:
|
Finally, let's install SDL. As we will be targeting an as-yet unreleased version, we'll need to compile the library ourselves. You can do this by downloading the latest version of the source code from SDL's GitHub page using the web interface, or alternatively you can get the latest copy of the code by installing git:
|
|
This will download a copy of the very latest version of the code onto your machine into a new folder called SDL.
The --depth=1
flag here tells git not to bother downloading the full history of the code, only the current state, making the download much quicker.
Whichever method you used, browser or git, there is a guide on how to compile the code inside SDL's doc
folder named README-cmake.md
.
The gist of it is that you need to perform 3 steps: configuring, compiling, and installing:
1. |
|
2. |
|
3. |
|
4. |
|
5. |
|
We move our terminal into the SDL folder.
Line 2 performs our configuration: checking any dependencies are met, writing configuration files about what features are present on the system, and configures the build process such that the root folder of the source code ("-S") is here ("."), and to build it into a new folder here called build.
The third line then performs the actual compiling of the code, and the fourth line installs SDL system-wide so it is available to g++.
Our final line makes a call to ldconfig
, which updates our system cache about which libraries are currently available.
Makefiles
With our libraries installed, let's build a test program to check everything is working properly. To do this, we'll make use of a makefile. If you've never used or heard of these before, don't worry. A makefile is basically a file which explains how your program should be built. Perhaps you have traditionally compiled your code by typing something like this into a terminal:
|
Where g++
is our C++ compiler, main.cpp
is our source code file, and we tell it to name the output executable my_program
.
However, this is a pretty trivial example. Sometimes we have many, many files that need to be compiled, many libraries to link, etc. When things become more complicated, we don't want to keep typing out this command in full each time we want to compile, so that's where makefiles come in! And they're incredibly simple too.
Our example above can be rewritten as the following makefile:
1. |
|
2. |
|
NOTE: Indentations in makefiles must be full TAB indentations! Using spaces as indentations will not work. I don't want this website to start any political arguments about spaces vs tabs so let's just leave it at that.
To compile your hypothetical main.cpp
code using this, you can save the above example as a regular text file named Makefile
(note the capital "M", and no file extension) in the same directory as your code.
To run it, we just need to be in the same directory in our terminal, and type make
.
Its as easy as that!
So what does the keyword all
mean here?
Well, the makefile allows us to specify different targets, which are just components that we can build individually.
This way, if you had a particularly large project, you could just type into your terminal make gui
and just that particular part of your code can be (re-)compiled.
By convention, we name the target which compiles everything, the whole program, as "all", and place it as the first target in our Makefiles.
If there is not a specific target typed into the terminal, make will run the first target found in the makefile, which by convention should compile the whole program.
So in our example above, if we typed make all
into our terminal, make would look for the the indented block of commands after the all
target in our makefile and run those, building our program.
Alternatively, if we typed just make
, by virtue of being the first target of perhaps many defined in the makefile, the same all
target would be run.
Note that the commands here for each target are just ordinary terminal commands.
Therefore you are not limited to just compiling things with g++
, but can run regular commands like cd
, ls
, or zip
here across multiple indented lines.
Makefiles also allow you to set up many other things, such as dependencies, but I will keep this lesson simple for now.
Let's introduce a few variables into our makefile to make it easier to customise in the future:
1. |
|
2. |
|
3. |
|
4. |
|
5. |
|
6. |
|
7. |
|
8. |
|
9. |
|
10. |
|
11. |
|
12. |
|
Alright, that's quite a lot of variables actually! Let's take it line by line.
We started by defining a variable to hold the name of our compiler, CC
.
The idea is that if you are compiling lots of things and at some point wish to change the compiler, you just need to edit this one variable, instead of every line where something is compiled.
We define a variable to hold the names of all the objects we wish to compile (source files), and one to hold the names of the libraries we want to link against.
In this case we link against the SDL library (-lSDL3), OpenGL (-lGL) and to GLEW (-lGLEW).
Another variable is used to hold any compiler flags we want to use - in this case just -Wall
which means to throw all compiler warnings so we get into good habits from the start!
We variablise the output program name too.
If you don't provide a name, g++ will give your executable the fairly ugly default name of a.out
, so we'll use this variable to give your program a reasonable name.
Finally, at the very bottom of our makefile, we write what's known as the recipe for our target, similar to what we did before.
After the target's name, all
, we start by adding the $(OBJS)
variable on the same line to make it a dependency.
The $(OBJS)
variable will immediately be substituted for each filename of our source code.
Make will therefore know that those files must exist in order to continue compiling.
Then to actually compile our program, we just combine the rest of our variables together to form our compile command.
If we save this file and run it (terminal, go to the right folder, type make
), make will display the commands it is running, and we will see that the above script ends up being translated to this in the command line:
1. |
|
Which is exactly what we want!
It's far easier to now just type make
each time we want to compile than typing out the full command each time.
Realistically the only time we will ever change this file is if we need to compile an extra source code file, or perhaps if we link to another library.
Cleaning Up
There is one more thing I want to show you about makefiles before we start building a real program.
Quite often, compiling creates files which "pollute" our workspace. This can be temporary files from our text editor, other temporary files, or perhaps object files if you choose to do some really fancy compiling procedure. Even the executable file.
Therefore it is common to add a target to your makefile called "clean". The idea is that this will just go through and delete all the unnecessary files you have accumulated. It will get your workspace back to a "pure" state, untouched by your compiler.
Right now, the only thing I'll do is to remove the executable, but you can modify this later if you want to remove anything else. Let's add it to the bottom of our makefile:
1. |
|
2. |
|
3. |
|
4. |
|
5. |
|
6. |
|
7. |
|
8. |
|
9. |
|
10. |
|
11. |
|
12. |
|
13. |
|
14. |
|
15. |
|
Now, when someone types make clean
into the terminal, make will look for a target with that name, and run the corresponding terminal commands, in this case deleting our executable file.
The -f
flag ensures if the file does not exist, no error messages will be shown, so it is fine to run make clean
repeatedly.
Testing our set-up
With the above makefile now in place, let's perform a quick test that everything is working correctly before moving on with our lessons.
Copy-and-paste the following code into a file named main.cpp
in the same folder as the makefile:
1. |
|
2. |
|
3. |
|
4. |
|
5. |
|
6. |
|
7. |
|
8. |
|
9. |
|
10. |
|
11. |
|
Don't worry about the actual code too much here - we cover it in the next lesson. For now we just want to check the compiler can find our libraries and build our program.
Open a terminal, go to your workspace and run make
.
That will compile the program, and hopefully no errors appear.
Then run ./00-Introduction-and-Setting-Up
or whatever you chose to name your executable, and if everything is working you should see "We are running!" appear in your terminal.
If something goes wrong, make sure you have SDL installed properly and no errors occurred during installation.
Make sure that both the makefile and main.cpp
are in the same directory, and that you are currently in that folder in your terminal when typing make
.
Also remember that a Makefile needs to be named with a capital "M", and uses only tabs for indentations - no spaces!
Alternatively, if you're having problems you can download and try my version of the files using the buttons just below, and try to compile those.
Otherwise if everything is working then you can move onto the next tutorial!