Introduction to OpenGL (C++, SDL, GL2)

Welcome to the wonderful world of graphics!

This tutorial will try to be a gentle introduction to the world of graphics, using OpenGL 2 (with no extensions), SDL, and C++. In this tutorial, we'll make a basic OpenGL application that draws a few simple objects. I'll only hit the highlights, but you can find the full source at the bottom of the page.

I will try to be mostly compiler- and platform-agnostic, though the code was last tested (a long time ago) only on Windows. For simplicity, the algorithms here are suboptimal and that there's a minimum of error checking. All of the concepts that follow apply when developing OpenGL applications with Python (and probably any other language too). Note that there is Python PyOpenGL basecode on the tutorials page. You may wish to read this tutorial while keeping in mind the graphics pipeline tutorial.


Preliminaries

I'll assume you have OpenGL 2, SDL (1 or 2), and a C++ toolchain set up on your system.

Now, we need a source file. This tutorial will have exactly one for simplicity. I called mine "main.cpp".

Next, you need some #includes. Specifically, we need to include SDL, OpenGL, and OpenGL's utility library "GLU". SDL comes in two major versions, SDL 1 and SDL 2. SDL 2 should be preferred. On Windows, you'll need to link against "opengl32.lib", "glu32.lib", "SDL.lib"/"SDL2.lib", and "SDLmain.lib"/"SDL2main.lib". If you're feeling lazy and you're using MSVC, the #pragmas below will do the job. Otherwise, add those libraries to your linker's settings. On Linux, or other Unix, you should use CMake to find_package "OpenGL" and "SDL".

//Don't worry about this; it's needed to make GL.h work properly on Windows only (and then, only
//	sometimes).
#include <Windows.h>

#include <GL/GL.h>
#include <GL/GLU.h>

//Choose whether to use SDL1 or SDL2
#if 0
	#include <SDL1/include/SDL.h>
	#include <SDL1/include/SDL_opengl.h>
	#pragma comment(lib,"SDL.lib")
	#pragma comment(lib,"SDLmain.lib")
#else
	#include <cstdio>
	#include <SDL2/include/SDL.h>
	#include <SDL2/include/SDL_opengl.h>
	#pragma comment(lib,"SDL2.lib")
	#pragma comment(lib,"SDL2main.lib")
	static SDL_Window* window;
	static SDL_Renderer* renderer;
#endif
#pragma comment(lib,"opengl32.lib")
#pragma comment(lib,"glu32.lib")

Initialization and Deinitialization

Make your main declaration (the right way), initialize SDL, and make a window. While making a window, we make an 'OpenGL context'. For now, all you need to know is that everything in OpenGL needs a context. In SDL 1, the context is made automatically. In SDL 2, we have to make it explicitly. For SDL 1, the only other tricky thing is the 32, which is the total number of color bits per pixel. The rest of the main function goes in the area I commented out in this listing. Once we're done with the program, we delete everything we made and then deinitialize SDL.

//Initialize everything, but don't catch fatal signals; give them to the OS.
SDL_Init( SDL_INIT_EVERYTHING | SDL_INIT_NOPARACHUTE );
#if   SDL_MAJOR_VERSION == 1
	//Sets the caption of the window created on the next line
	SDL_WM_SetCaption( "SDL and OpenGL example - Agatha Mallett", nullptr );
	//Creates the window
	SDL_SetVideoMode( screen_size[0],screen_size[1], 32, SDL_OPENGL );
#elif SDL_MAJOR_VERSION == 2
	//Creates the window
	window = SDL_CreateWindow(
		"SDL and OpenGL example - Agatha Mallett",
		SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
		screen_size[0], screen_size[1],
		SDL_WINDOW_OPENGL
	);
	//Create an OpenGL context.  In SDL 1, this was done automatically.
	SDL_GLContext context = SDL_GL_CreateContext(window);
#else
	#error
#endif

/* . . . */

//Clean up
#if   SDL_MAJOR_VERSION == 1
	//Normally you'd need to use `SDL_FreeSurface(⋯)`, but the `SDL_Surface*` returned by
	//	`SDL_SetVideoMode(⋯)` is special, and so it is cleaned up for us.
#elif SDL_MAJOR_VERSION == 2
	SDL_GL_DeleteContext(context);
	SDL_DestroyWindow(window);
#else
	#error
#endif
SDL_Quit();

Jumping inside that snipped section in the above code . . .

At this point, we now have an OpenGL context. OpenGL contexts are required for OpenGL calls to work.

The first thing we do is change OpenGL's state to enable depth testing of the depth buffer. If you're not familiar with the depth buffering algorithm, you should see the graphics pipeline tutorial—but in short, the idea is that it allows objects to figure out which is closest to the camera and which therefore should be seen for each pixel.

//Objects need to test each other to see which one is in front.  If you don't do this, you'll
//	"see through" things!
glEnable(GL_DEPTH_TEST);

Draw Code

The function for getting the user's input is pretty straightforward, so we'll now jump to the draw() function.

The first thing we do is set the draw region:

//Drawing to an area from the bottom left, `screen_size[0]` wide, and `screen_size[1]` high.
glViewport( 0,0, screen_size[0],screen_size[1] );

From here, we set up OpenGL's projection matrix, in the manner the comments describe. Again, the pipeline tutorial is relevant, but suffice to say OpenGL's projection matrix (kinda) transforms objects from the world, where they're drawn, to the screen, where you can see them. One tricky thing is the 0.1 and 100.0 arguments to gluPerspective(⋯). Put simply, these are the nearest and farthest distances, respectively, that you'll be able to see anything—and, for technical reasons, they should be set as close to each other as is practical. Additionally, the near value should be as far from 0.0 as possible.

//OpenGL is a state machine.  Tell it that future commands changing the matrix are to change
//	OpenGL's projection matrix.
glMatrixMode(GL_PROJECTION);
//Reset the projection matrix.
glLoadIdentity();
//Multiply a 45° perspective projection matrix into OpenGL's projection matrix.
gluPerspective( 45.0, (double)(screen_size[0])/(double)(screen_size[1]), 0.1,100.0 );

Now, we need to set up the camera. This is done by multiplying OpenGL's modelview matrix by a transform that makes it look like you're in a camera at the given position. In reality, you're still at the origin; it's all the objects that get transformed to make it look like you moved!

//Tell OpenGL that future commands changing the matrix are to change the modelview matrix.
glMatrixMode(GL_MODELVIEW);
//Reset the modelview matrix.
glLoadIdentity();
//Multiply OpenGL's modelview matrix with a transform matrix that simulates a camera at ⟨2,3,4⟩
//	looking towards the location ⟨0,0,0⟩ with up defined to be ⟨0,1,0⟩.
gluLookAt( 2.0,3.0,4.0, 0.0,0.0,0.0, 0.0,1.0,0.0 );

Now, we clear the screen so we can start rendering the frame from a blank image. We clear both the color (what you can see) and the depth buffer (an invisible quantity at each pixel that tells OpenGL how far away everything is; this is necessary for depth testing to work properly).

//Clear the screen's color and depth (default color is black, but you can change it with
//	`glClearColor(⋯)`).
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

From here, we draw a few objects and cycle the framebuffer, as described pretty clearly in the comments.


Source Listing

On Windows, SDL requires its DLLs to run. If you don't have them, here are some DLLs for x86 builds: (dlls.7z).

Full source listing follows. Enjoy!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//Don't worry about this; it's needed to make GL.h work properly on Windows only (and then, only
//	sometimes).
#include <Windows.h>

#include <GL/GL.h>
#include <GL/GLU.h>

//Choose whether to use SDL1 or SDL2
#if 0
	#include <SDL1/include/SDL.h>
	#include <SDL1/include/SDL_opengl.h>
	#pragma comment(lib,"SDL.lib")
	#pragma comment(lib,"SDLmain.lib")
#else
	#include <cstdio>
	#include <SDL2/include/SDL.h>
	#include <SDL2/include/SDL_opengl.h>
	#pragma comment(lib,"SDL2.lib")
	#pragma comment(lib,"SDL2main.lib")
	static SDL_Window* window;
	static SDL_Renderer* renderer;
#endif
#pragma comment(lib,"opengl32.lib")
#pragma comment(lib,"glu32.lib")



static int const screen_size[2] = { 800, 600 };

static bool get_input()
{
	SDL_Event event;
	while ( SDL_PollEvent(&event) )
	{
		switch ( event.type )
		{
			case SDL_QUIT: return false; //The little X in the window got pressed
			case SDL_KEYDOWN:
				if ( event.key.keysym.sym == SDLK_ESCAPE ) return false;
				break;
			default: break;
		}
	}
	return true;
}
static void draw()
{
	//Drawing to an area from the bottom left, `screen_size[0]` wide, and `screen_size[1]` high.
	glViewport( 0,0, screen_size[0],screen_size[1] );

	//OpenGL is a state machine.  Tell it that future commands changing the matrix are to change
	//	OpenGL's projection matrix.
	glMatrixMode(GL_PROJECTION);
	//Reset the projection matrix.
	glLoadIdentity();
	//Multiply a 45° perspective projection matrix into OpenGL's projection matrix.
	gluPerspective( 45.0, (double)(screen_size[0])/(double)(screen_size[1]), 0.1,100.0 );

	//Tell OpenGL that future commands changing the matrix are to change the modelview matrix.
	glMatrixMode(GL_MODELVIEW);
	//Reset the modelview matrix.
	glLoadIdentity();
	//Multiply OpenGL's modelview matrix with a transform matrix that simulates a camera at ⟨2,3,4⟩
	//	looking towards the location ⟨0,0,0⟩ with up defined to be ⟨0,1,0⟩.
	gluLookAt( 2.0,3.0,4.0, 0.0,0.0,0.0, 0.0,1.0,0.0 );

	//Clear the screen's color and depth (default color is black, but you can change it with
	//	`glClearColor(⋯)`).
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	//Begin drawing triangles.  Every subsequent triplet of vertices will be interpreted as a single
	//	triangle.  OpenGL's default color is white ⟨1.0,1.0,1.0⟩, so that's what color the triangle
	//	will be.
	glBegin(GL_TRIANGLES);
	//Three vertices follow, these will form a triangle
	glVertex3f( 0.0f, 0.1f, 0.0f); //Vertex at ⟨ 0.0, 0.1, 0.0⟩
	glVertex3f(-0.1f,-0.1f, 0.7f); //Vertex at ⟨-0.1,-0.1, 0.7⟩
	glVertex3f( 1.0f,-0.2f, 0.0f); //Vertex at ⟨ 1.0,-0.2, 0.0⟩
	//Done drawing triangles
	glEnd();

	//Now we're going to draw some lines to show the cardinal axes.  Every subsequent pair of
	//	vertices will be a single line.
	glBegin(GL_LINES);
	//All subsequent vertices will be red.
	glColor3f(1.0f,0.0f,0.0f);
	glVertex3f(0.0f,0.0f,0.0f); glVertex3f(1.0f,0.0f,0.0f);
	//All subsequent vertices will be green.
	glColor3f(0.0f,1.0f,0.0f);
	glVertex3f(0.0f,0.0f,0.0f); glVertex3f(0.0f,1.0f,0.0f);
	//All subsequent vertices will be blue.
	glColor3f(0.0f,0.0f,1.0f);
	glVertex3f(0.0f,0.0f,0.0f); glVertex3f(0.0f,0.0f,1.0f);
	//Since OpenGL thinks the color is blue now, all subsequent vertices will be blue.  But, we want
	//	the triangle above to be white the *next* time we call this function!  So, reset the color
	//	to white.
	glColor3f(1.0f,1.0f,1.0f);
	glEnd();

	//OpenGL works best double-buffered.  SDL automatically sets that up for us.  This will draw
	//	what we have just drawn to the screen so that we can see it.
	#if   SDL_MAJOR_VERSION == 1
		SDL_GL_SwapBuffers();
	#elif SDL_MAJOR_VERSION == 2
		SDL_GL_SwapWindow(window);
	#else
		#error
	#endif
}

int main( int argc, char* argv[] )
{
	//Initialize everything, but don't catch fatal signals; give them to the OS.
	SDL_Init( SDL_INIT_EVERYTHING | SDL_INIT_NOPARACHUTE );
	#if   SDL_MAJOR_VERSION == 1
		//Sets the caption of the window created on the next line
		SDL_WM_SetCaption( "Basic SDL and OpenGL - Agatha Mallett", nullptr );
		//Creates the window
		SDL_SetVideoMode( screen_size[0],screen_size[1], 32, SDL_OPENGL );
	#elif SDL_MAJOR_VERSION == 2
		//Creates the window
		window = SDL_CreateWindow(
			"Basic SDL and OpenGL - Agatha Mallett",
			SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
			screen_size[0], screen_size[1],
			SDL_WINDOW_OPENGL
		);
		//Create an OpenGL context.  In SDL 1, this was done automatically.
		SDL_GLContext context = SDL_GL_CreateContext(window);
	#else
		#error
	#endif

	//We now have an OpenGL context, and can call OpenGL functions.

	//Objects need to test each other to see which one is in front.  If you don't do this, you'll
	//	"see through" things!
	glEnable(GL_DEPTH_TEST);

	//Main application loop
	while (true)
	{
		if (!get_input()) break;
		draw();
	}

	//Clean up
	#if   SDL_MAJOR_VERSION == 1
		//Normally you'd need to use `SDL_FreeSurface(⋯)`, but the `SDL_Surface*` returned by
		//	`SDL_SetVideoMode(⋯)` is special, and so it is cleaned up for us.
	#elif SDL_MAJOR_VERSION == 2
		SDL_GL_DeleteContext(context);
		SDL_DestroyWindow(window);
	#else
		#error
	#endif
	SDL_Quit();

	//Return success; program exits
	return 0;
}