Ian Mallett

Welcome to the wonderful world of graphics!

This tutorial with try to be a gentle introduction to the world of graphics, using OpenGL 2 (with no extensions), SDL, and C++. Installation of these packages is not described, since there are plenty of descriptions already online, most of them work, and most are dependent on what development tools you're using. By contrast, I will try to be mostly compiler- and platform-agnostic, only describing the source file and what's goes into it--though a few platform-specific oddities are mentioned for completeness. This is supposed to be a tutorial, so pro coders may notice that many of 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 (which is admittedly at a very detailed level).

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.

First, we'll need a source file. This tutorial will have exactly one for simplicity. I called mine "main.cpp". I'm pretty sure C99 will work, if, for some spectacularly bizarre reason, you don't have a C++ compiler. C90 might even work with a bit of modification, too.

Next, you need some "#include"s. 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 "#pragma"s will do the job. Otherwise, add it to your linker's settings. On Linux, or other Unix, you should use CMake to find_package "OpenGL" and "SDL".

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

#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")

Make your main declaration (the right way), initialize SDL, and make a window. While making a window, we make an OpenGL context, which is the context under which OpenGL commands execute. For now, all you need to know is that you need one. 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 number of bits per pixel. Since OpenGL is doing the work, the value you put there probably doesn't matter much. 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 - Ian Mallett",NULL);
		//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 - Ian 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);

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

The first thing we do is clear the screen. We clear both the color (what you can see) and the depth buffer (an invisible quanitity 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 can change with glClearColor(...))
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

From here, we set up a viewport and 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.

	//Drawing to an area starting at 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 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);

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

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

Full source listing follows. Enjoy!

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

#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(void) {
	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;
		}
	}
	return true;
}
static void draw(void) {
	//Clear the screen's color and depth (default color is black, but can change with glClearColor(...))
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

	//Drawing to an area starting at 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 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);

	//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("SDL and OpenGL example - Ian Mallett",NULL);
		//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 - Ian 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;
}


COMMENTS
Ian Mallett - Contact -
Donate
- 2016 - Creative Commons License