OpenGL: Setting Up SDL 2

  • Requirement: SDL 2, OpenGL (comes with any C++ compiler), GLEW
  • Language: C++
  • Difficulty: Intermediate
  • Written by: Lee Zhi Eng
  • Last update: 18 Mar 2019

Introduction

In this tutorial, we will learn how to create a modern OpenGL 3.x renderer with SDL 2.

If you don’t already know what is OpenGL, it is basically an open standard API (Application Programming Interface) that lets you render 2D and 3D images on screen by empowering your computer’s GPU (Graphics Processing Unit). You don’t need to install OpenGL as most of the C++ development tools (Visual Studio, Qt Creator, etc.) already supported it by default. Do note that we will be using the modern OpenGL 3 instead of the older, deprecated version of OpenGL 2 which has a very different coding syntax.

SDL 2 on the other hand, is a cross-platform development library that helps you to easily create a native application window that acts as the container for your OpenGL rendering. It also provides other functionalities such as input, image loading and audio. For the most basic setup, you’ll only need to to link SDL2 and SDL2main to your C++ project.

Additionally, you also need GLEW (The OpenGL Extension Wrangler Library) to automatically generate OpenGL functions based on the extensions supported by your chosen OpenGL profile and version.

Once you have downloaded SDL 2 and link it to your C++ project, let’s get started!

Initialize SDL 2

1. First, include the header files to your source code. You can also force the compiler to load OpenGL and GLU libraries if you’re running MSVC compiler:

#include <string>
#include <GL/glew.h>
#include <SDL.h>

#ifdef _MSC_VER
	#pragma comment(lib, "opengl32.lib")
	#pragma comment(lib, "glu32.lib")
#endif

By including glew.h, you no longer have to include SDL_opengl.h, gl.h and glu.h.

2. Next, we must add in argc and argv to our main() function:

int main(int argc, char *argv[])
{
	(void)(argc);
	(void)(argv);
	
	return 0;
}

We used (void) to remove warning messages about unused variables. We must declare argc and argv even if we don’t use this 2 variables as SDL will not work without it.

3. After that, we initialize SDL by calling SDL_Init(). If any error happens and prevented SDL from initializing, we call the SDL_GetError() function to obtain the error message and display it on a message box by calling SDL_ShowSimpleMessageBox():

if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
{
	std::string msg = "SDL failed to initialize: ";
	msg.append(SDL_GetError());
	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Init Failed", msg.c_str(), nullptr);
	return 0;
}
else
{
	// Create Window here
}

4. Once we’re done with that, let’s create the SDL window:

window = SDL_CreateWindow("My Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
if(window == nullptr)
{
	std::string msg = "Window could not be created: ";
	msg.append(SDL_GetError());
	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Init Failed", msg.c_str(), nullptr);
	return 0;
}
else
{
	// Create OpenGL context here
}

From the code above, we created a resizable window (not full screen) at 640×480 size, which positioned at the center of the screen. We also set the window title as “My Game”. To read more about these options, please visit here.

Create OpenGL Context

1. Before we create the OpenGL context, we must tell SDL which profile and version of OpenGL we’re going to use in our application. We also declared some variables which we will be using later:

bool running = true;
SDL_Window* window;
SDL_GLContext context;
SDL_Event event;

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

if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
{
	... ... ...

There are 2 different types of OpenGL profiles – Compatibility Profile and Core Profile. The Compatibility Profile is for old software that uses legacy OpenGL functionality and needs backward compatibility, while all other modern software should use the Core Profile instead to fully utilize the modern GPUs in our machines.

Since the Core Profile starts at OpenGL 3.1 and above, we tell SDL we will be using that version as well.

2. After that, we can then proceed to create the OpenGL context by calling SDL_GL_CreateContext() after we successfully created the window. We also call glewInit() to automatically initialize OpenGL functions that are supported by the OpenGL version we just declared:

context = SDL_GL_CreateContext(window);
if(context == nullptr)
{
	std::string msg = "OpenGL context could not be created: ";
	msg.append(SDL_GetError());
	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Init Failed", msg.c_str(), nullptr);
	return 0;
}
else
{
	glewInit();
}

3. Then, we use a while-loop to keep the program running. We only close the program when we click on the [x] button on our SDL window:

else
{
	glewInit();
}

while (running)
{
	while(SDL_PollEvent(&event) != 0)
	{
		if(event.type == SDL_QUIT)
		{
			running = false;
		}
	}
}

4. Finally, we destroy the context and window before we quit the program by calling SDL_Quit():

	SDL_GL_DeleteContext(context);
	SDL_DestroyWindow(window);
	window = nullptr;
	SDL_Quit();

	return 0;
}

Phew, what a long ride. We have successfully created an empty window. However, it wasn’t very exciting to look at an empty window. So, let’s render a simple shape using OpenGL!

OpenGL Rendering

1. First, we add the following code to our while-loop:

while (running)
{
	while(SDL_PollEvent(&event) != 0)
	{
		if(event.type == SDL_QUIT)
		{
			running = false;
		}
	}

	glClearColor(0.2f, 0.2f, 0.25f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	render();

	SDL_GL_SwapWindow(window);
}

We first set the window background to dark gray by calling glClearColor() and glClear(). Then, we do all the rendering in our custom function called render(). We created the render() function because the rendering code is way too long to write directly in the while-loop. We will talk about the render() function in a while.

After we have rendered everything, we call SDL_Get_SwapWindow() to display the rendering result on the SDL window.

2. In the render() function, let’s declare an array called vertices. We use this array to store the position information of individual points that form a triangle:

GLfloat vertices[] =
{
	-0.5f, -0.5f, 0.0f,
	0.5f, -0.5f, 0.0f,
	0.0f,  0.5f, 0.0f
};

3. After that, we declare a vertex shader and a fragment shader:

static const char *vertexShader =
"#version 330 core\n"
"layout(location = 0) in vec2 posAttr;\n"
"void main() {\n"
"gl_Position = vec4(posAttr, 0.0, 1.0); }";

static const char *fragmentShader =
"#version 330 core\n"
"out vec4 col;\n"
"void main() {\n"
"col = vec4(1.0, 0.0, 0.0, 1.0); }";

A vertex shader is a set of code that tells GPU how to process the vertex information before passing it to the fragment shader. A fragment shader, on the other hand, is another set of code that tells GPU what color and shade to render within the given polygons.

In this example demo, we simply draws a basic triangle shape that contains 3 vertices. Then, we tell the GPU to color it fully with a plain red color.

4. Then, we tell the GPU to create a vertex shader and a fragment shader. Once the shaders have been successfully created, the GPU will return us the IDs of the shaders for our future reference. We then load the shaders code to the GPU and compile them by providing the IDs back to GPU so that it can know which shader we want to compile:

GLuint vertexShaderID = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShaderID, 1, &vertexShader, nullptr);
glCompileShader(vertexShaderID);

GLuint fragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShaderID, 1, &fragmentShader , nullptr);
glCompileShader(fragmentShaderID);

5. Right after that, we create a shader program and attach both the vertex and fragment shaders to it. Then, we “link” the shader program (store the shader program on GPU) and release the shaders, since we no longer need it anymore:

GLuint shaderProgramID = glCreateProgram();
glAttachShader(shaderProgramID, vertexShaderID);
glAttachShader(shaderProgramID, fragmentShaderID);
glLinkProgram(shaderProgramID);

glDetachShader(shaderProgramID, vertexShaderID);
glDetachShader(shaderProgramID, fragmentShaderID);
glDeleteShader(vertexShaderID);
glDeleteShader(fragmentShaderID);

6. Next, create a Vertex Buffer on the GPU and store the vertices array into it. We must also tell the GPU what size is the buffer so that it can render it correctly afterwards.

GLuint vertexBufferID;
glGenBuffers(1, &vertexBufferID);
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
glBufferData(GL_ARRAY_BUFFER, 9 * sizeof(GLfloat), &vertices[0], GL_STATIC_DRAW);

7. Before we render the triangle, we must call the following functions:

glUseProgram(shaderProgramID);

glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);

We called glUseProgram() to tell GPU we want to use that particular shader in the following rendering steps. Then, we also enabled the first vertex attribute array, which we declared in our vertex shader previously, namely posAttr. We know it is the first attribute because of the layout(location = 0) written at the front of the attribute in the vertex shader.

Then, glBindBuffer() is called to tell GPU we want to use this particular vertex buffer for the following rendering steps. Lastly, we use glVertexAttribPointer() to tell GPU the structure of our vertex buffer, which in this case, contains an array of data that is divided between every 3 GL_FLOAT data.

8. Then, we simply call glDrawArrays() to start the rendering process:

glDrawArrays(GL_TRIANGLES, 0, 3);

9. Finally, we disable the shader program and vertex attribute array when done rendering:

glUseProgram(NULL);
glDisableVertexAttribArray(0);

Build and run your program. You should now be able to see the following result:

Our first triangle in OpenGL

Hooray! After written such a long code, we are finally able to render a… triangle with OpenGL and SDL 2!

Source Code

The full source code looks like the following:

#include <string>
#include <GL/glew.h>
#include <SDL.h>

#ifdef _MSC_VER
	#pragma comment(lib, "opengl32.lib")
	#pragma comment(lib, "glu32.lib")
#endif

void render();

int main(int argc, char *argv[])
{
	(void)(argc);
	(void)(argv);

	bool running = true;
	SDL_Window* window;
	SDL_GLContext context;
	SDL_Event event;

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

	if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
	{
		std::string msg = "SDL failed to initialize: ";
		msg.append(SDL_GetError());
		SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Init Failed", msg.c_str(), nullptr);
		return 0;
	}
	else
	{
		window = SDL_CreateWindow("My Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
		if(window == nullptr)
		{
			std::string msg = "Window could not be created: ";
			msg.append(SDL_GetError());
			SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Init Failed", msg.c_str(), nullptr);
			return 0;
		}
		else
		{
			context = SDL_GL_CreateContext(window);

			if(context == nullptr)
			{
				// Display error message
				std::string msg = "OpenGL context could not be created: ";
				msg.append(SDL_GetError());
				SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Init Failed", msg.c_str(), nullptr);
				return 0;
			}
			else
			{
				glewInit();
			}
		}
	}

	while (running)
	{
		while(SDL_PollEvent(&event) != 0)
		{
			if(event.type == SDL_QUIT)
			{
				running = false;
			}
		}

		glClearColor(0.2f, 0.2f, 0.25f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		render();

		SDL_GL_SwapWindow(window);
	}

	SDL_GL_DeleteContext(context);
	SDL_DestroyWindow(window);
	window = nullptr;
	SDL_Quit();

	return 0;
}

void render()
{
	GLfloat vertices[] =
	{
		-0.5f, -0.5f, 0.0f,
		0.5f, -0.5f, 0.0f,
		0.0f,  0.5f, 0.0f
	};

	static const char *vertexShader =
	"#version 330 core\n"
	"layout(location = 0) in vec2 posAttr;\n"
	"void main() {\n"
	"gl_Position = vec4(posAttr, 0.0, 1.0); }";

	static const char *fragmentShader =
	"#version 330 core\n"
	"out vec4 col;\n"
	"void main() {\n"
	"col = vec4(1.0, 0.0, 0.0, 1.0); }";

	GLuint vertexShaderID = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShaderID, 1, &vertexShader, nullptr);
	glCompileShader(vertexShaderID);

	GLuint fragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShaderID, 1, &fragmentShader , nullptr);
	glCompileShader(fragmentShaderID);

	GLuint shaderProgramID = glCreateProgram();
	glAttachShader(shaderProgramID, vertexShaderID);
	glAttachShader(shaderProgramID, fragmentShaderID);
	glLinkProgram(shaderProgramID);

	glDetachShader(shaderProgramID, vertexShaderID);
	glDetachShader(shaderProgramID, fragmentShaderID);
	glDeleteShader(vertexShaderID);
	glDeleteShader(fragmentShaderID);

	GLuint vertexBufferID;
	glGenBuffers(1, &vertexBufferID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glBufferData(GL_ARRAY_BUFFER, 9 * sizeof(GLfloat), &vertices[0], GL_STATIC_DRAW);

	glUseProgram(shaderProgramID);

	glEnableVertexAttribArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);

	glDrawArrays(GL_TRIANGLES, 0, 3);

	glUseProgram(NULL);
	glDisableVertexAttribArray(0);
}

In the next tutorial (coming soon), we will learn how to render a 3D cube with OpenGL.

Leave a Reply