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.
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
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::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.
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_FILTERdoesn't use mipmaps
GL_TEXTURE_WRAP_Tare both set to
otherwise the texture will always return black.
Let's do that for our textures.
Displaying a sprite
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
framebuffer / renderbuffer / reuse-that-as-textures / ... ?
Optimizing for 2D
Since we work in 2D, we can remove:
- back-face culling is not necessary
- depth-test is not necessary
Remove the z coordinate
We can remote 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.xyw(), mvp.xyw(), mvp.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
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
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.
- "2d" from the OpenGL wikibook examples: https://gitlab.com/wikibooks-opengl/modern-tutorials/tree/master/2d
- Simple implementation of 2D blit in the GLtron project:
- AGPL'd implementation of 2D blits with optional palette emulation in the GNU FreeDink project:
- SDL2 and SFML both provide an OpenGL-accelerated 2D API.
- "glTexParameter". Khronos.org. https://www.khronos.org/opengles/sdk/docs/man/xhtml/glTexParameter.xml. Retrieved 2015-08-19.
- "OpenGL ES Common Profile Specification Version 2.0.25". Khronos.org. 2010-11-02. http://www.khronos.org/registry/gles/specs/2.0/es_full_spec_2.0.25.pdf. Retrieved 2011-11-12. - section 3.4.1 Basic Line Segment Rasterization
- "OpenGL FAQ and Troubleshooting Guide v1.2001.11.01 - 9.030 How do I draw 2D controls over my 3D rendering?". http://www.opengl.org/resources/faq/technical/transformations.htm#tran0030. Retrieved 2011-11-12. - If exact pixelization is required, you might want to put a small translation in the ModelView matrix