DEV Community

Ali Sherief
Ali Sherief

Posted on

2D OpenGL for Beginners

Note: Although this post only references C++, all of these concepts work equally as well for C.

I got the idea for this article while helping a friend drawing 2D shapes inside his OpenGL program. So I had an idea: Why not blog about it for others to use?

While I cannot link to the program itself, the goal of this article is to familiarize you with the primitive 2D drawing functions of OpenGL.

We will be using a special library called GLUT which booststraps the OpenGL initialization and shutdown sequence, which is otherwise complicated to code if done directly with OpenGL functions. GLUT provides you an easy way to draw OpenGL primitives while automatically creating the window to display it in. There are also alternative OpenGL windowing libraries available such as GLFW.

Header files

To start using GLUT, you must include the following header file:

#include <GL/glut.h>
Enter fullscreen mode Exit fullscreen mode

This header file belongs to GLUT and it includes all of the required OpenGL headers that all OpenGL programs need to run, so you don't have to include them yourself.

I must mention that different operating systems will place the OpenGL header files in different paths. So depending on the OS you are using, you may have to include two or three potential paths for the OpenGL header files, and use #ifdef to turn off the include files that don't exist. GL/glut.h handles all of this for you so that no matter whether you are using Windows, macOS or Linux, you only need to include one standard header file to use GLUT.

Initialization

There are two functions that you must call in succession to fully initialize GLUT.

  • glutInit(&argc, argv) - this takes the argc and argv of the C++ main() function, except that the first argument is a pointer to argc.

  • glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) - This tells OpenGL the method it should render drawings on a window. These flags tell it to use a single buffer (GLUT_SINGLE) and use the RGBA - Red, Green, Blue and Alpha - color system (GLUT_RGB).

  • glClearColor(r, g, b, a) - Sets the background color of the OpenGL window that's created. glClearColor(1.0, 1.0, 1.0, 0.0) makes the background white.

Creating a window

  • glutCreateWindow("My Window") - Creates a window with the title My Window.

Display and Resize functions

These two functions are the most important in any OpenGL program. The display function is responsible for drawing shapes on the window, and the resize function is responsible for synchronizing the OpenGL program's viewport (internal screen size) with the size of the window, and optionally rotating/translating/otherwise transforming all of the shapes, whenever the window is resized by the user.

Both of these functions are written by the programmer. They also need to make sure these functions run as fast as possible, because they will be called several times per second and if they run too slowly, users will notice lag in the program.

Resize function

  • glutReshapeFunc(resizefunc) - Pass the name of the resize function you made as resizefunc. The resize function has the following signature:
void screensize(GLsizei w, GLsizei h) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Where w and h are aliases (typedef's) of int, so you can treat these as integers and use them anywhere where an int is accepted.

Below is a sample resize function. It resizes the viewport to the window size whenever it changes.

void screensize(GLsizei w, GLsizei h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-250.0, 250.0, -250.0, 250.0, 250.0, -250.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
Enter fullscreen mode Exit fullscreen mode
  • glViewport(0, 0, w, h) - Ensures that the viewport is the same size as the window. Changing these values makes a different sized viewport that may be bigger or smaller than the window and thus cause lots of surrounding whitespace or objects to be cropped, respectively.
  • glMatrixMode - Changes the matrix which subsequent OpenGL functions work with. There are four matrices defined, GL_MODELVIEW, GL_PROJECTION, GL_TEXTURE and GL_COLOR. The resize function only works with the first two.

In this case we switch to the projection matrix, which is where all of the coordinates for our drawings are stored. We are going to resize this matrix to (-250,-250,-250)..(250,250,250).

  • glLoadIdentity() - OpenGL does not reset the matrix when you switch to a different one, so the current matrix may have values left over from its last use. This function clears it to the identity matrix, which is equivalent to no transformation at all.

  • glOrtho(-250.0, 250.0, -250.0, 250.0, 250.0, -250.0) - This sets the size of the projection matrix to the above size. One consequence of this is that all shapes have to be drawn within this range to be visible. This is then scaled to the viewport size after applying the modelview matrix to the projection transformation.

Then we load the modelview matrix and set it to the identity transformation. That concludes the resize function.

About the projection and modelview matrices

When you draw a shape with OpenGL, you specify the coordinates of the vertices you want to draw and the kind of shape that you want to draw (there are line, triangle, quad aka. rectangle, and polygon options available among other shapes). This shape is drawn on the modelview matrix, which is like one big coordinate grid where shapes can be plotted, except this is not shown on the screen. It's stored internally in the program.

This coordinate grid can be represented as a matrix if each element represents an arbitrarily small x,y point that can be plotted on the grid. This means that if we apply any matrix transformation on this grid, all of the shapes inside will be similarly possible. OpenGL supports exactly that.

The modelview matrix is changed to look around the 3D scene with an eyeball located in front of the computer. While the projection matrix is used to navigate around the 3D scene using a hidden camera inside the computer.

The difference between the two is that the eyeball can move left/right, up/down and forward/backward. While the camera can rotate left/right, up/down and zoom in/out. These two sets are fundamentally different movements.

Obviously, modelview forward/background and projection left/right and up/down are meaningless for 2D scenes, and don't have to be implemented.

A moving and rotating camera facing a duck
Image credits: OpenGL Camera by Song Ho Ahn

The position values are what's changed by a modelview matrix. While the rotation values are changed by the projection matrix. Not shown is the zoom value, which is also changed by projection. Note that only the first two rotation values need to be implemented to get full rotation (X and Y, where in OpenGL, X rotates left and right, while Y rotates up and down). You don't need to implement rotation around Z (i.e. rotating forward and backward) because that's the same as X.

Display function

  • glClear(GL_COLOR_BUFFER_BIT) - This is required. It wipes the color information from the previous rendering (if any) from the screen and resets the whole screen to the background color. I said it's required, so that you don't draw your shapes over shapes from the previous rendering.

For example, if you move 2D or 3D shapes around, the entire trail will be visible on the screen if you don't clear the color buffer bit.

As I mentioned before this is where shapes are drawn.

  • glColor3f(r, g, b) - This sets the color of the OpenGL painter. The next shapes the OpenGL draw will be in this color until this function is called again with a different color. r, g and b represent red, green and blue channels and are floats between 0 and 1 inclusive.
  • glColor4f(r, g, b, a) - Same as glColor3f but with an additional alpha channel.

  • glBegin(shape) and glEnd() - shape is an OpenGL primitive (in OpenGL-speak) such as GL_POINTS, GL_LINES, GL_TRIANGLES, GL_QUADS, or GL_POLYGON. glBegin() instructs OpenGL to use the vertices that are drawn subsequently until the next glEnd() call to create the shape.

  • glVertex2f(x, y) - Sends a vertex to OpenGL. Must be used between glBegin() and glEnd(). x and y are floats within the viewport size. If you changed the scene size in the projection matrix then these coordinates must be within that range or they won't be visible.

  • glVertex3f(x, y, z) - Same as glVertex2f but with a z coordinate for 3D scenes.

You will be writing glColor3/4f, glVertex2/3f, glBegin and glEnd calls several times (possibly even hundreds or thousands of times depending on how many shapes you're drawing) as part of the basic design of an OpenGL program. Of course, for and while loops help ease the writing.

  • glFlush() - Flushes all of the previous drawing commands, instructing OpenGL to render them on the screen. It is not guaranteed that shapes will be drawn on the screen until you call glFlush(), becuse the instructions may be stored together inside an intermediate buffer for performance reasons.

Conclusion

OpenGL may look difficult at first - after all, there aren't a lot of tutorials for it, but once you get the hang of it, you have enormous power in your hands. You will be able to draw any object on the screen and create scenes full of complex objects, especially if you know trigonometry and use a 3D model loading library to import 3D models in your program.

If you see any errors in this post, please let me know so I can correct them.

Top comments (0)