~/imallett (Ian Mallett)

This tutorial adds a simple diffuse texture to the triangle in the previous tutorial.

I removed extraneous comments from the sections covered in the last tutorial, and added a few new functions. These are explained in detail in the source, so it suffices to give just a brief overview of what we're doing.

A texture is just an image that is mapped onto a triangle. The general flow is to load the image from disk (or generate it programmatically), then upload it to the GPU (where it is then called a "texture"), and then draw with it.

How do you map the texture onto a triangle? To do this, you must understand texture coordinates. The image below (source), should show the general idea:

The cyan square on the left represents the texture coordinates. Imagine that the whole image is squished into that square so that it fits exactly. The cyan form on the right represents the polygons we want to map the image onto. To do this, for each of the vertices, we define a texture coordinate, which is the location in the texture to draw at that physical point.

In the drawing above, notice how at vertex v0, they have mapped the texture coordinate (0,0). This means that at the location v0, you'll see the bottom left of the image. At vertex v2, they have mapped the texture coordinate (1,1). So, at the location v2, you'll see the top right of the image. And so on. Try to think of stretching the image between the vertices.

This is not a tutorial on texture mapping, so I'll leave it here as to texturing theory.

One final note: Some authors choose (0,0) to be the top left of the image instead of the bottom left of the image. This is correct when dealing with images on the CPU (on the CPU, the first texel of an image is almost always the top left), but it is incorrect when dealing with OpenGL. People may have gotten confused since transferring your data to OpenGL effectively unflips the data, making it look like your image is inverted! OpenGL's design choice in this regard is consistent: note that additionally the y-axis points up instead of down (again opposite CPU graphics's convention).

Notice that in this example code, no effort is made to unflip the CPU data before it is passed to OpenGL. Hence, the texture is upsidedown. Remember this when examining the code.

The code is set up for compiling on MSVC. Some (minor) tweaking to the includes may be necessary on your platform. On Windows, SDL requires its DLLs to run. If you don't have them, here are some DLLs for x86 builds: (dlls.zip).

The data used in this tutorial is a scaled down image of the Mona Lisa I took from the public domain (direct link).

Full source listing follows:

#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 GLuint texture;

static void init_texture(void) {
	//Load the image from the file into SDL's surface representation
	SDL_Surface* surf = SDL_LoadBMP("texture.bmp");
	if (surf==NULL) { //If failed, say why and don't continue loading the texture
		printf("Error: \"%s\"\n",SDL_GetError()); return;
	}

	//Determine the data format of the surface by seeing how SDL arranges a test pixel.  This probably only works
	//	correctly for little-endian machines.
	GLenum data_fmt;
	Uint8 test = SDL_MapRGB(surf->format, 0xAA,0xBB,0xCC)&0xFF;
	if      (test==0xAA) data_fmt=         GL_RGB;
	else if (test==0xCC) data_fmt=0x80E0;//GL_BGR;
	else {
		printf("Error: \"Loaded surface was neither RGB or BGR!\""); return;
	}

	//Generate an array of textures.  We only want one texture (one element array), so trick
	//it by treating "texture" as array of length one.
	glGenTextures(1,&texture);
	//Select (bind) the texture we just generated as the current 2D texture OpenGL is using/modifying.
	//All subsequent changes to OpenGL's texturing state for 2D textures will affect this texture.
	glBindTexture(GL_TEXTURE_2D,texture);
	//Specify the texture's data.  This function is a bit tricky, and it's hard to find helpful documentation.  A summary:
	//   GL_TEXTURE_2D:    The currently bound 2D texture (i.e. the one we just made)
	//               0:    The mipmap level.  0, since we want to update the base level mipmap image (i.e., the image itself,
	//                         not cached smaller copies)
	//         GL_RGBA:    The internal format of the texture.  This is how OpenGL will store the texture internally (kinda)--
	//                         it's essentially the texture's type.
	//         surf->w:    The width of the texture
	//         surf->h:    The height of the texture
	//               0:    The border.  Don't worry about this if you're just starting.
	//        data_fmt:    The format that the *data* is in--NOT the texture!  Our test image doesn't have an alpha channel,
	//                         so this must be RGB.
	//GL_UNSIGNED_BYTE:    The type the data is in.  In SDL, the data is stored as an array of bytes, with each channel
	//                         getting one byte.  This is fairly typical--it means that the image can store, for each channel,
	//                         any value that fits in one byte (so 0 through 255).  These values are to be interpreted as
	//                         *unsigned* values (since 0x00 should be dark and 0xFF should be bright).
	// surface->pixels:    The actual data.  As above, SDL's array of bytes.
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surf->w,surf->h, 0, data_fmt,GL_UNSIGNED_BYTE,surf->pixels);
	//Set the minification and magnification filters.  In this case, when the texture is minified (i.e., the texture's pixels (texels) are
	//*smaller* than the screen pixels you're seeing them on, linearly filter them (i.e. blend them together).  This blends four texels for
	//each sample--which is not very much.  Mipmapping can give better results.  Find a texturing tutorial that discusses these issues
	//further.  Conversely, when the texture is magnified (i.e., the texture's texels are *larger* than the screen pixels you're seeing
	//them on), linearly filter them.  Qualitatively, this causes "blown up" (overmagnified) textures to look blurry instead of blocky.
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

	//Unload SDL's copy of the data; we don't need it anymore because OpenGL now stores it in the texture.
	SDL_FreeSurface(surf);
}
static void deinit_texture(void) {
	//Deallocate texture.  A lot of people forget to do this.
	glDeleteTextures(1,&texture);
}

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) {
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

	glViewport(0,0,screen_size[0],screen_size[1]);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(45.0, (double)(screen_size[0])/(double)(screen_size[1]), 0.1,100.0);
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	gluLookAt(2.0,3.0,4.0, 0.0,0.0,0.0, 0.0,1.0,0.0);

	//Set our loaded texture as the current 2D texture (this isn't actually technically necessary since our
	//texture was never unselected from above, but this is most clear)
	glBindTexture(GL_TEXTURE_2D,texture);
	//Tell OpenGL that all subsequent drawing operations should try to use the current 2D texture
	glEnable(GL_TEXTURE_2D);

	glBegin(GL_TRIANGLES);
	glTexCoord2f(0.0f,0.0f); //All subsequent vertices will have an associated texture coordinate of (0,0)
	glVertex3f( 0.0f, 0.1f, 0.0f);
	glTexCoord2f(1.0f,0.0f); //All subsequent vertices will have an associated texture coordinate of (1,0)
	glVertex3f(-0.1f,-0.1f, 0.7f);
	glTexCoord2f(0.0f,1.0f); //All subsequent vertices will have an associated texture coordinate of (0,1)
	glVertex3f( 1.0f,-0.2f, 0.0f);
	glEnd();

	//Tell OpenGL that all subsequent drawing operations should NOT try to use the current 2D texture
	glDisable(GL_TEXTURE_2D);

	glBegin(GL_LINES);
	glColor3f(1.0f,0.0f,0.0f); glVertex3f(0.0f,0.0f,0.0f); glVertex3f(1.0f,0.0f,0.0f);
	glColor3f(0.0f,1.0f,0.0f); glVertex3f(0.0f,0.0f,0.0f); glVertex3f(0.0f,1.0f,0.0f);
	glColor3f(0.0f,0.0f,1.0f); glVertex3f(0.0f,0.0f,0.0f); glVertex3f(0.0f,0.0f,1.0f);
	glColor3f(1.0f,1.0f,1.0f);
	glEnd();

	#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[]) {
	SDL_Init(SDL_INIT_EVERYTHING|SDL_INIT_NOPARACHUTE);
	#if   SDL_MAJOR_VERSION == 1
		SDL_WM_SetCaption("SDL and OpenGL example - Ian Mallett",NULL);
		SDL_SetVideoMode(screen_size[0],screen_size[1], 32, SDL_OPENGL);
	#elif SDL_MAJOR_VERSION == 2
		window = SDL_CreateWindow("SDL and OpenGL example - Ian Mallett", SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, screen_size[0],screen_size[1], SDL_WINDOW_OPENGL);
		SDL_GLContext context = SDL_GL_CreateContext(window);
	#else
		#error
	#endif

	glEnable(GL_DEPTH_TEST);

	init_texture();

	while (true) {
		if (!get_input()) break;
		draw();
	}

	deinit_texture();

	#if   SDL_MAJOR_VERSION == 1
	#elif SDL_MAJOR_VERSION == 2
		SDL_GL_DeleteContext(context);
		SDL_DestroyWindow(window);
	#else
		#error
	#endif
	SDL_Quit();

	return 0;
}


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