OpenGL Programming/Modern OpenGL Tutorial 2D

Even if you do not plan to make a 3D game, and stick to 2D, OpenGL will still bring invaluable tools.

Hardware acceleration

edit

In the past, 2D graphic card provided hardware acceleration by allowing the programmer to store bitmaps and sprites directly in the card, along with a few primitives to perform basic copies (blits), with or without alpha blending.

Nowadays, these features are being replaced by OpenGL and its (more generic) textures. Programming in 2D in OpenGL is basically displaying textures facing the screen, with z coordinates always set to 0. This also introduces a greatly needed standardization for 2D acceleration (for instance, there is essentially no way to get 2D acceleration under GNU/Linux + X11).

This technique is used by several graphics libraries, including SFML, ClanLib or Gnash.

Setting up the 2D space

edit

We will use an orthographic projection matrix, where there is no perspective (objects far away look as big as near objects - you may have already seen this in technical drawing, or by typing Numpad 5 in Blender).

GLM provides glm::ortho to compute such a projection. Since we'll be manipulating pixels directly, let's use the size of the physical screen in pixels, rather than [-1, 1] as we previously did.

In addition, we've seen that OpenGL's vertical axis is bottom-to-top, while traditional 2D screens are top-to-bottom, so let's reverse the Y axis:

  // glm::ortho(left, right, bottom, top, [zNear, zFar])
  glm::mat4 projection = glm::ortho(0, screen_width,
                                    screen_height, 0);

But, since we're programming modern, shouldn't we forget about old-style 2D coordinates? Well, it happens that most graphic formats are top-to-bottom, too. TGA and BMP are bottom-to-top, but almost all other formats, and above all formats libraries (JPG, PNG, etc.), will give you top-to-bottom pictures. And we saw in the texture tutorial that OpenGL uses bottom-to-top for textures, so it would display them upside-down!

Reversing the OpenGL screen means that our pictures can be uploaded to the OpenGL graphic card as-is, and will be displayed in the right direction. The alternative would be to reverse texture coordinates.

The only point of attention is that the rotation angle on the Z axis need to be reversed, too.

But maybe the main reason behind reversing coordinates is that most users expect Y coordinates at top-to-bottom: check Gimp and Dia for instance; also check other 2D game frameworks and libraries. One notable exception is Inkscape (vector drawing) where coordinates start at the bottom-left like OpenGL.

Uploading textures

edit

Graphic cards used to have odd limits, such as only allowing power-of-two dimensions.

In OpenGL ES 2, non-power-of-two textures are allowed only if:

  • GL_TEXTURE_MIN_FILTER doesn't use mipmaps
  • GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T are both set to GL_CLAMP_TO_EDGE

otherwise the texture will always return black[1].

Let's do that for our textures.

Displaying a sprite

edit

To "blit" the texture to the OpenGL buffer the simplest way is to draw a pair of triangles with a texture:

/* code here */

You could however perform incremental display updates, by not calling glClear and using techniques such as dirty rectangles - although nowdays the GPU is usually fast enough to avoid implementing this kind of optimization.

Blitting on a texture

edit

framebuffer / renderbuffer / reuse-that-as-textures / ... ?

WIP

Optimizing for 2D

edit

Since we work in 2D, we can remove:

  • back-face culling is not necessary
  • depth-test is not necessary

Remove the z coordinate

edit

We can remove the z coordinates in the vertices to save space. Keep the w coordinate to 1, so we can work with transformation matrices.

Speaking of which, GLM only provides 4x4 transformation matrices. To shrink transformation matrices from 4x4 to 3x3 we can:

  • program a new set of functions to work with 3x3 matrices.
  • drop the 3rd line and the 3rd row:

This is a 3D affine matrix:

xx  yx  zx  tx
xy  yy  zy  ty
xz  yz  zz  tz
0   0   0   1

This is a 2D affine matrix:

xx  yx  tx
xy  yy  ty
0   0   1

We can code the 3D->2D conversion as:

#define GLM_SWIZZLE
#include <glm/glm.hpp>
...
glm::mat3 mvp2D(mvp[0].xyw(), mvp[1].xyw(), mvp[3].xyw());

gl_Position is still a vec4, so:

  gl_Position = mvp * vec4(v_coord, 1.0);

It doesn't really matter what v_coord.z is, since the ortho view always displays the texture the same, whatever z we use.

Zooming the whole screen

edit

You can perform a zoom effect by adjusting the projection. Here's a progressive zoom out, for instance:

  float scale = glutGet(GLUT_ELAPSED_TIME) / 1000.0 * .2;  // 20% per second
  glm::mat4 projection = glm::ortho(0.0f, 1.0f*screen_width*scale, 1.0f*screen_height*scale, 0.0f);

Exact / perfect pixelization

edit

OpenGL has a special rule to draw fragments at the center of pixel screens, called "diamond exit rule" [2] [3].

Consequently, it is recommended to add a small translation in X,Y before drawing 2D lines, so that the last pixel isn't missed:

glm::translate(glm::mat4(1), glm::vec3(0.375, 0.375, 0.));

TODO: provide an example

Note: this seems limited to drawing primitives, I couldn't reproduce any issue when manipulating textures.

edit

References

edit
  1. "glTexParameter". Khronos.org. Retrieved 2015-08-19.
  2. "OpenGL ES Common Profile Specification Version 2.0.25" (PDF). Khronos.org. 2010-11-02. Retrieved 2011-11-12. - section 3.4.1 Basic Line Segment Rasterization
  3. "OpenGL FAQ and Troubleshooting Guide v1.2001.11.01 - 9.030 How do I draw 2D controls over my 3D rendering?". Retrieved 2011-11-12. - If exact pixelization is required, you might want to put a small translation in the ModelView matrix

< OpenGL Programming

Browse & download complete code