Windows Opengl Tutorial

Get started learning OpenGL on windows using C language.

Create a directory for your project. Download win32_opengl.h and place it in your directory.

win32_opengl.h takes care of OpenGL initialization:

void init_opengl(HWND *hwnd, HDC *hdc, HGLRC *hglrc, unsigned int window_width, unsigned int window_height, char *window_title);

NOTE: You should already be familiar with C programming on Windows OS. This tutorial assumes there is a version of the MSVC Visual Studio compiler installed on the machine. You must be able to launch a x64 version of Visual Studio Developer Command Prompt, that will be referred to as developer cmd.

To build the executable, from a developer cmd, we will navigate to the project directory, and call build.bat:

build

To get that out of the way, start by making a new file in your project directory and call it build.bat:

@echo off

cl -W4 -wd4100 -wd4189 -wd4101 -FC -nologo -Zi main.c

NOTE: Batch file programming is beyond the scope of this tutorial, suffice to say this will produce the executable we're looking for, with debug information, so we can debug it with Visual Studio.

Now, make a new file in your project directory called opengl_function_loader.h:

IMPORTANT: Make sure to call it exactly opengl_function_loader.h as it is automatically included from win32_opengl.h header (you will not be including this file yourself).

#ifndef OPENGL_FUNCTION_LOADER_H

#define OPENGL_FUNCTION_LOADER_H


#define GL_FUNCTIONS(X) \

X(PFNGLCREATEBUFFERSPROC, glCreateBuffers ) \

X(PFNGLBINDBUFFERPROC, glBindBuffer ) \

X(PFNGLNAMEDBUFFERSTORAGEPROC, glNamedBufferStorage ) \

X(PFNGLNAMEDBUFFERDATAPROC, glNamedBufferData ) \

X(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray ) \

X(PFNGLCREATEVERTEXARRAYSPROC, glCreateVertexArrays ) \

X(PFNGLVERTEXARRAYATTRIBBINDINGPROC, glVertexArrayAttribBinding ) \

X(PFNGLVERTEXARRAYVERTEXBUFFERPROC, glVertexArrayVertexBuffer ) \

X(PFNGLVERTEXARRAYATTRIBFORMATPROC, glVertexArrayAttribFormat ) \

X(PFNGLENABLEVERTEXARRAYATTRIBPROC, glEnableVertexArrayAttrib ) \

X(PFNGLCREATESHADERPROGRAMVPROC, glCreateShaderProgramv ) \

X(PFNGLGETPROGRAMIVPROC, glGetProgramiv ) \

X(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog ) \

X(PFNGLGENPROGRAMPIPELINESPROC, glGenProgramPipelines ) \

X(PFNGLUSEPROGRAMSTAGESPROC, glUseProgramStages ) \

X(PFNGLBINDPROGRAMPIPELINEPROC, glBindProgramPipeline ) \

X(PFNGLPROGRAMUNIFORMMATRIX4FVPROC, glProgramUniformMatrix4fv ) \

X(PFNGLNAMEDBUFFERSUBDATAPROC, glNamedBufferSubData ) \

X(PFNGLDEBUGMESSAGECALLBACKPROC, glDebugMessageCallback )


#define X(type, name) static type name;

GL_FUNCTIONS(X)

#undef X


#define STR2(x) #x

#define STR(x) STR2(x)


static PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB;

static PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB;

static PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT;

static PFNWGLGETEXTENSIONSSTRINGARBPROC wglGetExtensionsStringARB;


#endif //OPENGL_FUNCTION_LOADER_H

IMPORTANT: When you copy-paste the code above, it is possible that you will get extra empty lines between each line. That won't do, you will get a bunch of compiler errors that may be scary, but don't worry! Just make sure to remove any "added" empty lines from copy-paste and you will be fine.

NOTE: When you encounter a missing function pointer from OpenGL, come back to this file and include the function name to the list:

X(PFNGLCREATEBUFFERSPROC, glCreateBuffers ) \

X(PFNGLBINDBUFFERPROC, glBindBuffer ) \

X(PFNGLNAMEDBUFFERSTORAGEPROC, glNamedBufferStorage ) \

X(PFNGLNAMEDBUFFERDATAPROC, glNamedBufferData ) \

X(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray ) \

X(PFNGLCREATEVERTEXARRAYSPROC, glCreateVertexArrays ) \

X(PFNGLVERTEXARRAYATTRIBBINDINGPROC, glVertexArrayAttribBinding ) \

X(PFNGLVERTEXARRAYVERTEXBUFFERPROC, glVertexArrayVertexBuffer ) \

X(PFNGLVERTEXARRAYATTRIBFORMATPROC, glVertexArrayAttribFormat ) \

X(PFNGLENABLEVERTEXARRAYATTRIBPROC, glEnableVertexArrayAttrib ) \

X(PFNGLCREATESHADERPROGRAMVPROC, glCreateShaderProgramv ) \

X(PFNGLGETPROGRAMIVPROC, glGetProgramiv ) \

X(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog ) \

X(PFNGLGENPROGRAMPIPELINESPROC, glGenProgramPipelines ) \

X(PFNGLUSEPROGRAMSTAGESPROC, glUseProgramStages ) \

X(PFNGLBINDPROGRAMPIPELINEPROC, glBindProgramPipeline ) \

X(PFNGLPROGRAMUNIFORMMATRIX4FVPROC, glProgramUniformMatrix4fv ) \

X(PFNGLNAMEDBUFFERSUBDATAPROC, glNamedBufferSubData ) \

X(PFNGLDEBUGMESSAGECALLBACKPROC, glDebugMessageCallback )

I will not bother trying to explain to you the pattern on this list, you can easily find out by yourself and add new functions to it. Add the line and try to compile again, the missing function pointer error will go away.

NOTE: This file doesn't need to be separated like this, it could be pasted into win32_opengl.h (where it is included from) but since you will most likely need to add functions to the list of loaded functions later, it is nice to have it isolated like this.

Now make a new file in the project directory and call it main.c:

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

#include <windowsx.h>


#pragma comment (lib, "gdi32.lib")

#pragma comment (lib, "user32.lib")

Above we simply include some Windows programming headers, and include Windows DLLs using a useful feature of Visual Studio.

NOTE: You can find the full code for main.c here.

Then, we define the main Windows event callback as follows:

LRESULT CALLBACK

window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)

{

LRESULT result = 0;

switch (message)

{

case WM_SIZE:

{

} break;

case WM_DESTROY:

{

PostQuitMessage(0);

return 0;

} break;

default:

{

result = DefWindowProcA(window, message, wparam, lparam);

} break;

}

return result;

}

NOTE: Make sure not to change the name of this function, as it is called from win32_opengl.h header file. This tutorial assumes you know what this function is and what it does, if not, you can find information here.

Now it is time to include win32_opengl.h:

#include "win32_opengl.h"

Next, we define the main function, or the entrypoint, of the C program. Our example will have the following pseudo structure:

int WinMain(HINSTANCE instance, HINSTANCE prev_instance, LPSTR cmd_line, int cmd_show)

{

// declare variables

// call init_opengl()

// create shader for drawing points

// set on init opengl context state

// create opengl buffers for drawing points (vao and vbo)

while (game_running)

{

// process Windows OS events

// set per frame opengl context state

// set data for opengl vertex buffer

// draw to back buffer and swap

}

}

First, we declare variables to hold the important handles:

int game_running = 1;

HWND window;

HDC device_context;

HGLRC opengl_context;

Then, call the init_opengl function that win32_opengl.h header provides (it will set those handles for you):

init_opengl(&window, &device_context, &opengl_context, 800, 600, "Test window");

Create the shader for drawing points.

NOTE: You can find many tutorials about how the following code works. This tutorial is only concerned with providing a quick bootstrap so you can start learning OpenGL in C on Windows OS as quickly as possible.

char *pts_vertex_shader_source =

"#version 450 core \n"

"layout (location=0) in vec3 in_pos; \n"

"out gl_PerVertex { \n"

" vec4 gl_Position; \n"

"}; \n"

"void main() \n"

"{ \n"

"gl_Position = vec4(in_pos, 1); \n"

"} \n";


char *pts_fragment_shader_source =

"#version 450 core \n"

"layout (location=0) \n"

"out vec4 out_color; \n"

"void main() \n"

"{ \n"

"out_color = vec4(0,1,0,1); \n"

"} \n";


GLuint vertex_shader = glCreateShaderProgramv(GL_VERTEX_SHADER, 1, (char **)&pts_vertex_shader_source);

GLuint fragment_shader = glCreateShaderProgramv(GL_FRAGMENT_SHADER, 1, (char **)&pts_fragment_shader_source);


GLint linked;

glGetProgramiv(vertex_shader, GL_LINK_STATUS, &linked);

if (!linked)

{

char message[1024];

glGetProgramInfoLog(vertex_shader, sizeof(message), 0, message);

OutputDebugStringA(message);

assert(!"Failed to create vertex shader!");

}

glGetProgramiv(fragment_shader, GL_LINK_STATUS, &linked);

if (!linked)

{

char message[1024];

glGetProgramInfoLog(fragment_shader, sizeof(message), 0, message);

OutputDebugStringA(message);

assert(!"Failed to create fragment shader!");

}


GLuint shader_pipeline=(GLuint)-1;

glGenProgramPipelines(1, &shader_pipeline);

glUseProgramStages(shader_pipeline, GL_VERTEX_SHADER_BIT, vertex_shader);

glUseProgramStages(shader_pipeline, GL_FRAGMENT_SHADER_BIT, fragment_shader);

NOTE: Special thanks to Douglas for help with debugging on a Nvidia GPU, where we found that you can't use the prefix "gl_" in shader variable names (In older versions of this tutorial, that mistake was made in the code).


Set the on init context for opengl:

wglSwapIntervalEXT(1); // enable vsync

glEnable(GL_BLEND);

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // alpha blended

glDisable(GL_DEPTH_TEST);

glDisable(GL_CULL_FACE);

glPointSize(5);

Create the buffers:

GLuint pts_vao=(GLuint)-1;

glCreateVertexArrays(1, &pts_vao);


GLuint pts_vbo=(GLuint)-1;

glCreateBuffers(1, &pts_vbo);


// allocates gpu memory for vertex buffer (enough for a maximum number allowed of points)

glNamedBufferStorage(pts_vbo, 4096*sizeof(float)*3, 0, GL_DYNAMIC_STORAGE_BIT);


// bind vertex buffer to vertex array object

GLint vbo_index = 0;

glVertexArrayVertexBuffer(pts_vao, vbo_index, pts_vbo, 0, sizeof(float)*3);


// set vertex buffer input layout (remember pts_3d.bo[0] is the vao [vertex array object])

glVertexArrayAttribFormat(pts_vao, 0, 3, GL_FLOAT, GL_FALSE, 0);

glVertexArrayAttribBinding(pts_vao, 0, vbo_index);

glEnableVertexArrayAttrib(pts_vao, 0);

Now, entering the game loop:

ShowWindow(window, SW_SHOWDEFAULT);

while (game_running)

{

// process Windows OS events

// set per frame opengl context state

// set data for opengl vertex buffer

// draw to back buffer and swap

}

Process Windows OS events:

MSG message;

while (PeekMessageA(&message, 0, 0, 0, PM_REMOVE))

{

if (message.message == WM_QUIT)

{

game_running = 0;

break;

}

TranslateMessage(&message);

DispatchMessageA(&message);

}

Set "per-frame" OpenGL context:

glClearColor(0.1f, 0.1f, 0.1f, 1);

glClear(GL_COLOR_BUFFER_BIT);

glViewport(0,0,800,600);

glScissor(0,0,800,600);

glBindVertexArray(pts_vao);

Set data for OpenGL vertex buffer:

float pt_set[] =

{

-0.5f,-0.5f,-0.5f, // (x,y,z) for a single point

+0.5f,-0.5f,-0.5f,

+0.5f,+0.5f,-0.5f,

-0.5f,+0.5f,-0.5f,

};

unsigned int float_count = array_count(pt_set);

unsigned int pt_count = float_count / 3; // (x,y,z)

unsigned int pt_set_byte_size = sizeof(pt_set);

// copy from stack memory to gpu memory that we reserved earlier for vertex buffer

glNamedBufferSubData(pts_vbo, 0, pt_set_byte_size, pt_set);

Draw to "back buffer", and swap:

glBindProgramPipeline(shader_pipeline);

glDrawArrays(GL_POINTS, 0, pt_count);

SwapBuffers(device_context);


If you succeed, you will see something like this:

To finish, we add above window_proc:

unsigned int client_width = 0;

unsigned int client_height = 0;

And, inside window_proc:

LRESULT CALLBACK

window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)

{

LRESULT result = 0;

switch (message)

{

case WM_SIZE:

{

client_width = LOWORD(lparam);

client_height = HIWORD(lparam);

} break;

case WM_DESTROY:

{

PostQuitMessage(0);

return 0;

} break;

default:

{

result = DefWindowProcA(window, message, wparam, lparam);

} break;

}

return result;

}

NOTE: We are declaring client_width and client_height outside any function, so that we have it available in all functions below its definition, this is important because we need to assign new values (of client_width and client_height) from inside window_proc (which is a callback defined by Windows OS), and read (client_width and client_height) from our own functions later on. That means we need some method of communicating from inside window_proc callback to outside (our functions). We are using the simplest method here.

Then, inside the "game loop", at the "second block" we make the simple change:

// set per frame opengl context state

glClearColor(0.1f, 0.1f, 0.1f, 1);

glClear(GL_COLOR_BUFFER_BIT);

glViewport(0,0,800,600);

glScissor(0,0,800,600);

glBindVertexArray(pts_vao);

Becomes:

// set per frame opengl context state

glClearColor(0.1f, 0.1f, 0.1f, 1);

glClear(GL_COLOR_BUFFER_BIT);

glViewport(0,0,client_width,client_height);

glScissor(0,0,client_width,client_height);

glBindVertexArray(pts_vao);

That concludes this tutorial, now you should be set to start your learning journey.