OpenGL Texture Mapping (C++, SDL, GL2)
This tutorial shows how to add 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.
Texture Mapping
Intuitively, a '
A bit more formally, you load an image from disk (or generate it programmatically), then upload it to the GPU. With your help, the GPU adds specifications of how the image is to be accessed, forming a '

Figure 1
: An image with texture coordinates (left) gets mapped onto a 3D polygon (right). (Image source)The correspondence between (the '
The texture coordinate is given in a normalized space called \(\vecinline{s,t}\)-space, which ranges from \(\vecinline{0,0}\) to \(\vecinline{1,1}\), where the former is the lower-left of the image, and the latter is the upper-right. It's worth noting that many people call texture coordinates '\(\vecinline{u,v}\)-coordinates', but this is a mistake: \(\vecinline{u,v}\)-coordinates are similar, but are not normalized; this is much less convenient when modeling.
Figure 1 gives the basic 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. In the figure, notice how at vertex \(v_0\), the texture coordinate \(\vecinline{0,0}\) is mapped. This means that at the location \(v_0\), you'll see the bottom left of the image. At vertex \(v_2\), the texture coordinate \(\vecinline{1,1}\). So, at the location \(v_2\), you'll see the top right of the image. And so on.
Some authors choose \(\vecinline{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 the 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). In this example code, no effort is made to unflip the CPU data before it is passed to OpenGL. Hence, the texture is upsidedown.
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.7z).
The data used in this tutorial is a scaled down image of the Mona Lisa I took from the public domain:

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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//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 GLuint texture;
static bool load_texture( char const* bmp_path )
{
//Load the image from the file into SDL's surface representation
SDL_Surface* surf = SDL_LoadBMP( bmp_path );
if ( surf == nullptr )
{
// If failed, say why and don't continue loading the texture
printf( "Error: \"%s\"\n", SDL_GetError() );
return false;
}
//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: \"%s\"\n", "Loaded surface was neither RGB or BGR!" );
SDL_FreeSurface( surf );
return false;
}
//Generate an array of textures. We only want one texture (one element array).
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` : Says that the target of this operation should be the currently bound
2D texture (i.e. the one we just made and bound).
`0` : The mipmap level to specify. We aren't using mipmapping; the image
level is taken to be 0.
`GL_RGBA` : The internal format of the texture. This is how OpenGL will store the
texture internally; it's essentially the texture's type.
`surf->w` : The width of the image data and texture.
`surf->h` : The height of the image data and texture.
`0` : Don't define a border around the image.
`data_fmt` : The format that the *data* is in, *not* the texture! Our test image
doesn't have an alpha channel, so this is RGB.
`GL_UNSIGNED_BYTE`: The type the data is given in. The SDL image data is stored as an
array of unsigned bytes, with each channel getting one byte. This is
fairly typical.
`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. These tell OpenGL what to do when then the
texture's pixels (texels) are a different size that the screen pixels you're seeing them on
(i.e. they are minified or magnified). Here, we set it to linearly filter, meaning it takes the
four closest texels and linearly interpolates them. Mipmapping could give better results for
minification.
*/
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 has it in the texture.
SDL_FreeSurface( surf );
return true;
}
static void cleanup_texture()
{
//Deallocate texture (our array of one texture). A lot of people forget to do this.
glDeleteTextures( 1, &texture );
}
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()
{
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 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
//Set our loaded texture as the current 2D texture
glBindTexture( GL_TEXTURE_2D, texture );
//Tell OpenGL that all subsequent fixed-function drawing operations should use the current 2D
// texture.
glEnable( GL_TEXTURE_2D );
glBegin(GL_TRIANGLES);
//All subsequent vertices will have an associated texture coordinate of ⟨0,0⟩
glTexCoord2f(0.0f,0.0f);
glVertex3f( 0.0f, 0.1f, 0.0f);
//All subsequent vertices will have an associated texture coordinate of ⟨1,0⟩
glTexCoord2f(1.0f,0.0f);
glVertex3f(-0.1f,-0.1f, 0.7f);
//All subsequent vertices will have an associated texture coordinate of ⟨0,1⟩
glTexCoord2f(0.0f,1.0f);
glVertex3f( 1.0f,-0.2f, 0.0f);
glEnd();
//Tell OpenGL that all subsequent fixed-function drawing operations should *not* 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
}
static void run_demo()
{
glEnable(GL_DEPTH_TEST);
if (!load_texture( "texture.bmp" )) return;
while (true)
{
if (!get_input()) break;
draw();
}
cleanup_texture();
}
int main( int argc, char* argv[] )
{
SDL_Init( SDL_INIT_EVERYTHING | SDL_INIT_NOPARACHUTE );
#if SDL_MAJOR_VERSION == 1
SDL_WM_SetCaption( "Basic OpenGL Texturing - Agatha Mallett", nullptr );
SDL_SetVideoMode( screen_size[0],screen_size[1], 32, SDL_OPENGL );
#elif SDL_MAJOR_VERSION == 2
window = SDL_CreateWindow(
"Basic OpenGL Texturing - Agatha 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
run_demo();
#if SDL_MAJOR_VERSION == 1
#elif SDL_MAJOR_VERSION == 2
SDL_GL_DeleteContext(context);
SDL_DestroyWindow(window);
#else
#error
#endif
SDL_Quit();
return 0;
}