Drawing with OpenGL
- OpenGL Graphics Primitives
- Data in OpenGL Buffers
- Vertex Specification
- OpenGL Drawing Commands
- Instanced Rendering
Chapter Objectives
After reading this chapter, you will be able to:
- Identify all of the rendering primitives available in OpenGL.
- Initialize and populate data buffers for use in rendering geometry.
- Optimize rendering using advanced techniques like instanced rendering.
The primary use of OpenGL is to render graphics into a framebuffer. To accomplish this, complex objects are broken up into primitives—points, lines, and triangles that when drawn at high enough density give the appearance of 2D and 3D objects. OpenGL includes many functions for rendering such primitives. These functions allow you to describe the layout of primitives in memory, how many primitives to render, and what form they take, and even to render many copies of the same set of primitives with one function call. These are arguably the most important functions in OpenGL, as without them, you wouldn’t be able to do much but clear the screen.
This chapter contains the following major sections:
- “OpenGL Graphics Primitives” describes the available graphics primitives in OpenGL that you can use in your renderings.
- “Data in OpenGL Buffers” explains the mechanics of working with data in OpenGL.
- “Vertex Specification” outlines how to use vertex data for rendering, and processing it using vertex shaders.
- “OpenGL Drawing Commands” introduces the set of functions that cause OpenGL to draw.
- “Instanced Rendering” describes how to render multiple objects using the same vertex data efficiently.
OpenGL Graphics Primitives
OpenGL includes support for many primitive types. Eventually they all get rendered as one of three types—points, lines, or triangles. Line and triangle types can be combined together to form strips, loops (for lines), and fans (for triangles). Points, lines, and triangles are the native primitive types supported by most graphics hardware.^{1} Other primitive types are supported by OpenGL, including patches, which are used as inputs to the tessellator and the adjacency primitives that are designed to be used as inputs to the geometry shader. Tessellation (and tessellation shaders) are introduced in Chapter 9, and geometry shaders are introduced in Chapter 10. The patch and adjacency primitive types will be covered in detail in each of those chapters. In this section, we cover only the point, line, and triangle primitive types.
Points
Points are represented by a single vertex. The vertex represents a point in four-dimensional homogeneous coordinates. As such, a point really has no area, and so in OpenGL it is really an analogue for a square region of the display (or draw buffer). When rendering points, OpenGL determines which pixels are covered by the point using a set of rules called rasterization rules. The rules for rasterizing a point in OpenGL are quite straightforward—a sample is considered covered by a point if it falls within a square centered on the point’s location in window coordinates. The side length of the square is equal to the point’s size, which is fixed state (set with glPointSize()), or the value written to the gl_PointSize built-in variable in the vertex, tessellation, or geometry shader. The value written to gl_PointSize in the shader is used only if GL_PROGRAM_POINT_SIZE is enabled, otherwise it is ignored and the fixed state value set with glPointSize() is used.
The default point size is 1.0. Thus, when points are rendered, each vertex essentially becomes a single pixel on the screen (unless it’s clipped, of course). If the point size is increased (either with glPointSize(), or by writing a value larger than 1.0 to gl_PointSize), then each point vertex may end up lighting more than one pixel. For example, if the point size is 1.2 pixels and the point’s vertex lies exactly at a pixel center, then only that pixel will be lit. However, if the point’s vertex lies exactly midway between two horizontally or vertically adjacent pixel centers, then both of those pixels will be lit (i.e., two pixels will be lit). If the point’s vertex lies at the exact midpoint between four adjacent pixels, then all four pixels will be lit—for a total of four pixels being lit for one point!
Point Sprites
When you render points with OpenGL, the fragment shader is run for every fragment in the point. Each point is essentially a square area of the screen and each pixel can be shaded a different color. You can calculate that color analytically in the fragment shader or use a texture to shade the point. To assist in this, OpenGL fragment shaders include a special built-in variable called gl_PointCoord which contains the coordinate within the point where the current fragment is located. gl_PointCoord is available only in the fragment shader (it doesn’t make much sense to include it in other shaders) and has a defined value only when rendering points. By simply using gl_PointCoord as a source for texture coordinates, bitmaps and textures can be used instead of a simple square block. Combined with alpha blending or with discarding fragments (using the discard keyword
), it’s even possible to create point sprites with odd shapes.
We’ll revisit point sprites with an example shortly. If you want to skip ahead, the example is shown in “Point Sprites” on Page 346.
Lines, Strips, and Loops
In OpenGL, the term line refers to a line segment, not the mathematician’s version that extends to infinity in both directions. Individual lines are therefore represented by pairs of vertices, one for each endpoint of the line. Lines can also be joined together to represent a connected series of line segments, and optionally closed. The closed sequence is known as a line loop, whereas the open sequence (one that is not closed) is known as a line strip. As with points, lines technically have no area, and so special rasterization rules are used to determine which pixels should be lit when a line segment is rasterized. The rule for line rasterization is known as the diamond exit rule. It is covered in some detail in the OpenGL specification. However, we attempt to paraphrase it here. When rasterizing a line running from point A to point B, a pixel should be lit if the line passes through the imaginary edge of a diamond shape drawn inside the pixel’s square area on the screen—unless that diamond contains point B (i.e., the end of the line is inside the diamond). That way, if another, second line is drawn from point B to point C, the pixel in which B resides is lit only once.
The diamond exit rule suffices for thin lines, but OpenGL allows you to specify wider sizes for lines using the glLineWidth() function (the equivalent for glPointSize() for lines).
There is no equivalent to gl_PointSize for lines—lines are rendered at one fixed width until state is changed in OpenGL. When the line width is greater than 1, the line is simply replicated width times either horizontally or vertically. If the line is y-major (i.e., it extends further vertically than horizontally), it is replicated horizontally. If it is x-major then it is replicated vertically.
The OpenGL specification is somewhat liberal on how ends of lines are represented and how wide lines are rasterized when antialiasing is turned off. When antialiasing is turned on, lines are treated as rectangles aligned along the line, with width equal to the current line width.
Triangles, Strips, and Fans
Triangles are made up of collections of three vertices. When separate triangles are rendered, each triangle is independent of all others. A triangle is rendered by projecting each of the three vertices into screen space and forming three edges running between the edges. A sample is considered covered if it lies on the positive side of all of the half spaces formed by the lines between the vertices. If two triangles share an edge (and therefore a pair of vertices), no single sample can be considered inside both triangles. This is important because, although some variation in rasterization algorithm is allowed by the OpenGL specification, the rules governing pixels that lie along a shared edge are quite strict:
- No pixel on a shared edge between two triangles that together would cover the pixel should be left unlit.
- No pixel on a shared edge between two triangles should be lit by more than one of them.
This means that OpenGL will reliably rasterize meshes with shared edges without gaps between the triangles, and without overdraw.^{2} This is important when rasterizing triangle strips or fans. When a triangle strip is rendered, the first three vertices form the first triangle, then each subsequent vertex forms another triangle along with the last two vertices of the previous triangle. This is illustrated in Figure 3.1.
Figure 3.1. Vertex layout for a triangle strip
When rendering a triangle fan, the first vertex forms a shared point that is included in each subsequent triangle. Triangles are then formed using that shared point and the next two vertices. An arbitrarily complex convex polygon can be rendered as a triangle fan. Figure 3.2 shows the vertex layout of a triangle fan.
Figure 3.2. Vertex layout for a triangle fan
These primitive types are used by the drawing functions that will be introduced in the next section. They are represented by OpenGL tokens that are passed as arguments to functions used for rendering. Table 3.1 shows the mapping of primitive types to the OpenGL tokens used to represent them.
Table 3.1. OpenGL Primitive Mode Tokens
Primitive Type |
OpenGL Token |
Points |
GL_POINTS |
Lines |
GL_LINES |
Line Strips |
GL_LINE_STRIP |
Line Loops |
GL_LINE_LOOP |
Independent Triangles |
GL_TRIANGLES |
Triangle Strips |
GL_TRIANGLE_STRIP |
Triangle Fans |
GL_TRIANGLE_FAN |
Rendering Polygons As Points, Outlines, or Solids
A polygon has two sides—front and back—and might be rendered differently depending on which side is facing the viewer. This allows you to have cutaway views of solid objects in which there is an obvious distinction between the parts that are inside and those that are outside. By default, both front and back faces are drawn in the same way. To change this, or to draw only outlines or vertices, use glPolygonMode().
Reversing and Culling Polygon Faces
By convention, polygons whose vertices appear in counterclockwise order on the screen are called front facing. You can construct the surface of any “reasonable” solid—a mathematician would call such a surface an orientable manifold (spheres, donuts, and teapots are orientable; Klein bottles and Möbius strips aren’t)—from polygons of consistent orientation. In other words, you can use all clockwise polygons or all counterclockwise polygons.
Suppose you’ve consistently described a model of an orientable surface but happen to have the clockwise orientation on the outside. You can swap what OpenGL considers the back face by using the function glFrontFace(), supplying the desired orientation for front-facing polygons.
In a completely enclosed surface constructed from opaque polygons with a consistent orientation, none of the back-facing polygons are ever visible—they’re always obscured by the front-facing polygons. If you are outside this surface, you might enable culling to discard polygons that OpenGL determines are back-facing. Similarly, if you are inside the object, only back-facing polygons are visible. To instruct OpenGL to discard front- or back-facing polygons, use the command glCullFace() and enable culling with glEnable().
Advanced
In more technical terms, deciding whether a face of a polygon is front- or back-facing depends on the sign of the polygon’s area computed in window coordinates. One way to compute this area is
where x_{i} and y_{i} are the x and y window coordinates of the i^{th} vertex of the n-vertex polygon and where i ⊕ 1 is shorthand for (i + 1) mod n, where mod is the modulus operator.
Assuming that GL_CCW has been specified, if a > 0, the polygon corresponding to that vertex is considered to be front-facing; otherwise, it’s back-facing. If GL_CW is specified and if a < 0, then the corresponding polygon is front-facing; otherwise, it’s back-facing.