DMIoT School of Computing Software Engineering Academic Program
CHAPTER TWO
2. GRAPHICS PRIMITIVES
2.1. Introduction
All graphics packages construct pictures from basic building blocks known as graphics
primitives. Primitives that describe the geometry, or shape, of these building blocks are
known as geometric primitives. They can be anything from 2-D primitives such as points,
lines and polygons to more complex 3-D primitives such as spheres and polyhedra (a
polyhedron is a 3-D surface made from a mesh of 2-D polygons).
In the following sections we will examine some algorithms for drawing different
primitives, and where appropriate we will introduce the routines for displaying these
primitives in OpenGL.
2.2. OpenGL Point Drawing Primitives
The most basic type of primitive is the point. Many graphics packages, including
OpenGL, provide routines for displaying points. We have already seen the OpenGL
routines for point drawing in the simple OpenGL program introduced in Chapter 1. To
recap, we use the pair of functions glBegin … glEnd, using the symbolic constant
GL_POINTS to specify how the vertices in between should be interpreted. In addition,
the function glPointSize can be used to set the size of the points in pixels. The default
point size is 1 pixel. Points that have a size greater than one pixel are drawn as squares
with a side length equal to their size. For example, the following code draws three 2-D
points with a size of 2 pixels.
glPointSize(2.0);
glBegin(GL_POINTS);
glVertex2f(100.0, 200.0);
glVertex2f(150.0, 200.0);
glVertex2f(150.0, 250.0);
glEnd();
Note that when we specify 2-D points, OpenGL will actually create 3-D points with the
third coordinate (the z-coordinate) equal to zero. Therefore, there is not really any such
thing as 2-D graphics in OpenGL – but we can simulate 2-D graphics by using a constant
z-coordinate.
2.3. Line Drawing Algorithms
Lines are a very common primitive and will be supported by almost all graphics
packages. In addition, lines form the basis of more complex primitives such as polylines
(a connected sequence of straight-line segments) or polygons (2-D objects formed by
straight-line edges).
Lines are normally represented by the two end-points of the line, and points (x, y) along
the line must satisfy the following slope-intercept equation:
1|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
y = mx + c …………………………..…… (1)
where m is the slope or gradient of the line, and c is the coordinate at which the line
intercepts the y-axis. Given two end-points (x0, y0) and (xend, yend), we can calculate
values for m and c as follows:
y y0
m end ………………………………… (2)
xend x0
c y0 mx0 ………………………………… (3)
Furthermore, for any given x-interval δx, we can calculate the corresponding y-interval
δy:
δy = m.δx ………………………………… (4)
δx = δy/m ………………………………… (5)
These equations form the basis of the two line-drawing algorithms described below: the
DDA algorithm and Bresenham’s algorithm.
2.3.1. DDA Line-Drawing Algorithm
The Digital Differential Analyser (DDA) algorithm operates by starting at one end-point
of the line, and then using Eqs. (4) and (5) to generate successive pixels until the second
end-point is reached. Therefore, first, we need to assign values for δx and δy.
Before we consider the actual DDA algorithm, let us consider a simple first approach to
this problem. Suppose we simply increment the value of x at each iteration (i.e. δx = 1),
and then compute the corresponding value for y using Eqs. (2) and (4). This would
compute correct line points but, as illustrated by Figure 1, it would leave gaps in the line.
The reason for this is that the value of δy is greater than one, so the gap between
subsequent points in the line is greater than 1 pixel.
Figure 1 – ‘Holes’ in a Line Drawn by Incrementing x and Computing the Corresponding
y-Coordinate
The solution to this problem is to make sure that both δx and δy have values less than or
equal to one. To ensure this, we must first check the size of the line gradient. The
conditions are:
If |m| ≤ 1:
o δx = 1
o δy = m
If |m| > 1:
o δx = 1/m
o δy = 1
2|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
Once we have computed values for δx and δy, the basic DDA algorithm is:
Start with (x0, y0)
Find successive pixel positions by adding on (δx, δy) and rounding to the nearest
integer, i.e.
o xk+1 = xk + δx
o yk+1 = yk + δy
For each position (xk, yk) computed, plot a line point at (round(xk), round(yk)),
where the round function will round to the nearest integer.
Note that the actual pixel value used will be calculated by rounding to the nearest integer,
but we keep the real-valued location for calculating the next pixel position.
Let us consider an example of applying the DDA algorithm for drawing a straight-line
segment. Referring to see Figure 2, we first compute a value for the gradient m:
y y0 (13 10) 3
m end 0.6
xend x0 (15 10) 5
Now, because |m| ≤ 1, we compute δx and δy as follows:
δx = 1
δy = 0.6
Using these values of δx and δy we can now start to plot line points:
Start with (x0, y0) = (10, 10) – colour this pixel
Next, (x1, y1) = (10+1, 10+0.6) = (11, 10.6) – so we colour pixel (11, 11)
Next, (x2, y2) = (11+1, 10.6+0.6) = (12, 11.2) – so we colour pixel (12, 11)
Next, (x3, y3) = (12+1, 11.2+0.6) = (13, 11.8) – so we colour pixel (13, 12)
Next, (x4, y4) = (13+1, 11.8+0.6) = (14, 12.4) – so we colour pixel (14,12)
Next, (x5, y5) = (14+1, 12.4+0.6) = (15, 13) – so we colour pixel (15, 13)
We have now reached the end-point (xend, yend), so the algorithm terminates
Figure 2 - The Operation of the DDA Line-Drawing Algorithm
The DDA algorithm is simple and easy to implement, but it does involve floating point
operations to calculate each new point. Floating point operations are time-consuming
when compared to integer operations. Since line-drawing is a very common operation in
computer graphics, it would be nice if we could devise a faster algorithm which uses
integer operations only. The next section describes such an algorithm.
3|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
2.3.2. Bresenham’s Line-Drawing Algorithm
Bresenham’s line-drawing algorithm provides significant improvements in efficiency
over the DDA algorithm. These improvements arise from the observation that for any
given line, if we know the previous pixel location, we only have a choice of 2 locations
for the next pixel. This concept is illustrated in Figure 3: given that we know (xk, yk) is a
point on the line, we know the next line point must be either pixel A or pixel B. Therefore
we do not need to compute the actual floating-point location of the ‘true’ line point; we
need only make a decision between pixels A and B.
Figure 3 - Bresenham's Line-Drawing Algorithm
The algorithm can be divided into two parts:
For slope |m| < 1 and
For slope |m| > = 1
Case 1: For slope |m| < 1, do the following steps:
1. Read (xs, ys) and (xe, ye) as two end points of the line.
2. Calculate ∆x, ∆y and p as follows.
∆x = |xe – xs|
∆y = |ye –ys|
p = 2∆y - ∆x // p is decision parameter
3. Plot a point at (xs, ys) position
Plot (xs, ys)
4. If p < 0 Otherwise //if p >= 0
If xs < xe If xs < xe, then
xs = xs + 1 xs = xs + 1
Else Else
xs = xs – 1 xs = xs – 1
p = p + 2∆y If ys < ye, then
ys = ys + 1
Else
ys = ys – 1
p = p + 2(∆y -∆x)
5. Plot a point at (xs, ys) position
Plot(xs, ys)
6. Repeat steps 4 & 5 until xs = xe
4|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
Case 2: For slope |m| >= 1, do the following procedures:
1. Read (xs, ys) and (xe, ye) as two end points Otherwise //if p >= 0
2. Calculate ∆x, ∆y and p as follows. If ys < ye, then
∆x = |xe – xs| ys = ys + 1
∆y = |ye –ys| Else
p = 2∆x - ∆y // a decision parameter ys = ys – 1
3. Plot a point at (xs,ys) position If xs < xe, then
Plot(xs, ys) xs = xs + 1
4. If p < 0, then Else
If ys < ye, then xs = xs – 1
ys = ys + 1 p = p + 2(∆x -∆y)
Else 5. Plot a point at (xs, ys) position
ys = ys – 1 Plot(xs, ys)
p = p + 2∆x 6. Repeat steps 4 and 5 until ys = ye
Example: Indicate which rasterization would be chosen by Bresenham’s line drawing algorithm
when scan converting a line from screen coordinate (10, 10) to (15, 13); see figure 2.
Solution:
1. Let (xs, ys) = (10, 10) and (xe, ye) = (15, 13)
2. ∆x = |xe – xs| =|15 -10| = 5
∆y = |ye –ys| =|13 -10| = 3
p = 2∆y - ∆x (why? because |m| =3/5 <1)
=2x3–5=1>0
3. Plot a point at (xs, ys) position
Plot(xs, ys) = (10, 10);
xs = xs +1 = 10 + 1 = 11
ys = ys +1 = 10 + 1 = 11
p = p + 2(∆y - ∆x)
= 1 + 2(3 – 5) = -3 < 0
Plot(xs, ys) = (11, 11);
xs = xs +1 = 11 + 1 = 12
ys = ys = 11
p = p + 2∆y = -3+2*3 = 3 > 0
Plot(xs, ys) = (12, 11);
xs = xs +1 = 12 + 1 = 13
ys = ys +1 = 11 + 1 = 12
p = p + 2(∆y -∆x) = 3+2(3-5) = -1 < 0
Plot(xs, ys) = (13, 12);
xs = xs + 1 = 13 + 1 = 14
ys = ys = 12
p = p + 2∆y = -1 + 2(3) = 5 > 0
5|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
Plot(xs, ys) = (14, 12);
xs = xs + 1 = 14 + 1 = 15
ys = ys = 12 + 1 = 13
p = p + 2(∆y - ∆x) = 5 + 2(3 - 5) = 1 > 0
Plot(xs, ys) = (15, 13);
Since xs = 15 = xe (stop)
We can see that the algorithm plots exactly the same points as the DDA algorithm but it
computes them using only integer operations. For this reason, Bresenham’s algorithm is
the most popular choice for line-drawing in computer graphics.
2.3.3. OpenGL Line Functions
We can draw straight-lines in OpenGL using the same glBegin … glEnd functions that
we saw for point-drawing. This time we specify that vertices should be interpreted as line
end-points by using the symbolic constant GL_LINES. For example, the following code
glLineWidth(3.0);
glBegin(GL_LINES);
glVertex2f(100.0, 200.0);
glVertex2f(150.0, 200.0);
glVertex2f(150.0, 250.0);
glVertex2f(200.0, 250.0);
glEnd()
will draw two separate line segments: one from (100, 200) to (150, 200) and one from
(150, 250) to (200, 250). The line will be drawn in the current drawing colour and with a
width defined by the argument of the function glLineWidth.
Two other symbolic constants allow us to draw slightly different types of straight-line
primitive: GL_LINE_STRIP and GL_LINE_LOOP. The following example illustrates the
difference between the three types of line primitive. First we define 5 points as arrays of
2 GLint values. Next, we define exactly the same vertices for each of the three types of
line primitive. The images to the right show how the vertices will be interpreted by each
primitive.
GLint p1[] = {200,100}; GLint p2[] = {50,0};
GLint p3[] = {100,200}; GLint p4[] = {150,0};
GLint p5[] = {0,100};
glBegin(GL_LINES);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
6|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
glBegin(GL_LINE_STRIP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
glBegin(GL_LINE_LOOP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
We can see that GL_LINES treats the vertices as pairs of end-points. Lines are drawn
separately and any extra vertices (i.e. a start-point with no end-point) are ignored.
GL_LINE_STRIP will create a connected polyline, in which each vertex is joined to the
one before it and after it. The first and last vertices are only joined to one other vertex.
Finally, GL_LINE_LOOP is the same as GL_LINE_STRIP except that the last point is
joined to the first one to create a loop.
2.4. Circle-Drawing Algorithms
Before we examine algorithms for circle-drawing we will consider the mathematical
equations of a circle. In Cartesian coordinates we can write:
( x xc )2 ( y yc )2 r 2 …………………………… (6)
where (xc, yc) is the centre of the circle. Alternatively, in polar coordinates we can write:
x xc r cos …………………………………… (7)
y yc r sin …………………………………… (8)
In the following sections we will examine a number of approaches to plotting points on a
circle, culminating in the most efficient algorithm: the midpoint algorithm.
2.4.1. Plotting Points Using Cartesian Coordinates
As a first attempt at a circle-drawing algorithm we can use the Cartesian coordinate
representation. Suppose we successively increment the x-coordinate and calculate the
corresponding y-coordinate using Eq. (9):
y yc r 2 xc x ……………………………………
2
(9)
This would correctly generate points on the boundary of a circle. However, like the first
attempt at a line-drawing algorithm (see Figure 1) we would end up with ‘holes’ in the
line – see Figure 4. We would have the same problem if we incremented the y-coordinate
and plotted a calculated x-coordinate. As with the DDA line-drawing algorithm we can
overcome this problem by calculating and checking the gradient: if |m| ≤ 1 then
increment x and calculate y, and if |m| > 1 then increment y and calculate x. However,
7|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
with the DDA algorithm we only needed to compute and check the gradient once for the
entire line, but for circles the gradient changes with each point plotted, so we would need
to compute and check the gradient at each iteration of the algorithm. This fact, in addition
to the square root calculation in Eq. (9), would make the algorithm quite inefficient.
Figure 4 - Circle-Drawing by Plotting Cartesian Coordinates
2.4.2. Plotting Points Using Polar Coordinates
An alternative technique is to use the polar coordinate equations. Recall that in polar
coordinates we express a position in the coordinate system as an angle θ and a distance r.
For a circle, the radius r will be constant, but we can increment θ and compute the
corresponding x and y values according to Eqs. (7) and (8).
For example, suppose we want to draw a circle with (xc, yc) = (5, 5) and r = 10. We start
with θ = 0o and compute x and y as:
x = 5 + 10 cos 0o = 15
y = 5 + 10 sin 0o = 5
Therefore we plot (15,5)
Next, we increase θ to 5o:
x = 5 + 10 cos 5o = 14.96
y = 5 + 10 sin 5o = 5.87
Therefore we plot (15, 6)
This process would continue until we had plotted the entire circle (i.e. θ = 360o). Using
this polar coordinate technique, we can avoid holes in the boundary if we make small
enough increases in the value of θ. In fact, if we use θ = 1/r (where r is measured in
pixels, and θ in radians) we will get points exactly 1 pixel apart and so there is guaranteed
to be no holes.
This algorithm is more efficient than the Cartesian plotting algorithm. It can be made
even more efficient, at a slight cost in quality, by increasing the size of the steps in the
value of θ and then joining the computed points by straight-line segments (see Figure 5).
Figure 5 - Circle-Drawing by Plotting Polar Coordinates
8|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
2.4.3. Taking Advantage of the Symmetry of Circles in Plotting
We can improve the efficiency of any circle-drawing algorithm by taking advantage of
the symmetry of circles. As illustrated in Figure 6, when we compute the Cartesian
coordinates x and y of points on a circle boundary we have 4 axes of symmetry (shown in
blue): we can generate a point reflected in the y-axis by negating the x-coordinate; we can
generate a point reflected in the x-axis by negating the y-coordinate, and so on. In total,
for each point computed, we can generate seven more through symmetry. Computing
these extra points just by switching or negating the coordinates of a single point is much
more efficient than computing each boundary point separately. This means that we only
need to compute boundary points for one octant (i.e. one eighth) of the circle boundary –
shown in red in Figure 6.
In addition to generating extra points, the 4-way symmetry of circles has another
advantage if combined with the Cartesian plotting algorithm. Recall that this algorithm
resulted in holes in some parts of the circle boundary (see Figure 4), which meant that a
time-consuming gradient computation had to be performed for each point. In fact, the
problem of holes in the boundary only occurs when the gradient is greater than 1 for
computing x-coordinates, or when the gradient is less than or equal to 1 for computing y-
coordinates. Now that we know we only need to compute points for one octant of the
circle we do not need to perform this check. For example, in the red octant in Figure 6,
we know that the gradient will never become greater than one, so we can just increment
the y-coordinate and compute the corresponding x-coordinates.
Figure 6 - Four-Way Symmetry of Circles
However, even with these efficiency improvements due to symmetry, we still need to
perform a square root calculation (for Cartesian plotting) or a trigonometric calculation
(for polar plotting).
2.5. Ellipse-Drawing Algorithms
An ellipse usually looks like a squashed circle (in fact a circle is a special kind of ellipse).
Any point P on an ellipse has the same sum of distances to two "focus" points.
9|Page Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
Figure 7 - Ellipse Terminologies
The long axis (in this case, rx) is known as the major axis of the ellipse; the short axis (in
this case ry) is known as the minor axis. The ratio of the major to the minor axis lengths is
called the eccentricity of the ellipse.
Starting by placing an ellipse in the coordinate system with the foci on the x axis
equidistant from the origin at F’(-c, 0) and F(c, 0), as shown in figure below.
Figure 7 - Ellipse Drawing
Some graphics packages may provide routines for drawing ellipses, although as we will
see ellipses cannot be drawn as efficiently as circles. The equation for a 2-D ellipse in
Cartesian coordinates is:
2
x xc y yc
2
1 …………………………… (10)
r
x y
r
Alternatively, in polar coordinates, we can write:
x = xc + rx cos θ …………………………………… (11)
y = yc + ry sin θ …………………………………… (12)
Notice that Eqs. (10)-(12) are similar to the circle equations given in Eqs. (6)-(8), except
that ellipses have two radius parameters: rx and ry.
Here is the algorithm for ellipse drawing
1. Get the center point as (xc, yc)
2. Get the lengths of semi-major and semi-minor axes as r1 & r2
3. Calculate the value of t
t = PI/180
10 | P a g e Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
4. Initialize i to zero (i = 0)
5. Compute the value for d
d=i*t
6. Compute the values for x and y
x = xc+r1*sin(d)
y = yc+r2*cos(d)
7. Plot a point at (x, y) position
8. Increment the value of i
i = i+1
9. Repeat steps (5) to (8) until i<360
2.6. Fill-Area Primitives
The most common type of primitive in 3-D computer graphics is the fill-area primitive.
The term fill-area primitive refers to any enclosed boundary that can be filled with a solid
colour or pattern. However, fill-area primitives are normally polygons, as they can be
filled more efficiently by graphics packages. Polygons are 2-D shapes whose boundary is
formed by any number of connected straight-line segments. They can be defined by three
or more coplanar vertices (coplanar points are positioned on the same plane). Each pair
of adjacent vertices is connected in sequence by edges. Normally polygons should have
no edge crossings: in this case they are known as simple polygons or standard polygons
(see Figure 9).
Figure 9 - A Polygon with an Edge Crossing
Polygons are the most common form of graphics primitive because they form the basis of
polygonal meshes, which is the most common representation for 3-D graphics objects.
Polygonal meshes approximate curved surfaces by forming a mesh of simple polygons.
Some examples of polygonal meshes are shown in Figure 10.
Figure 10 - Examples of Polygonal Mesh Surfaces
2.6.1. Convex and Concave Polygons
We can differentiate between convex and concave polygons:
Convex polygons have all interior angles ≤ 180o
Concave polygons have at least one interior angle > 180o
11 | P a g e Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
The difference between convex and concave polygons is shown in Figure 11. Many
graphics packages (including OpenGL) require all fill-area polygons to be convex
polygons. The reason for this is that it is much more efficient to fill a polygon if we know
it is convex. Some packages even require that all fill-area primitives are triangles, as this
makes filling even more straightforward.
(a) (b)
Figure 11 – Types of Polygon: (a) Convex; (b) Concave
Most graphics packages also insist on some other conditions regarding polygons.
Polygons may not be displayed properly if any of the following conditions are met:
The polygon has less than 3 vertices; or
The polygon has collinear vertices; or
The polygon has non-coplanar vertices; or
The polygon has repeated vertices.
Polygons that meet one of these conditions are often referred to as degenerate polygons.
Degenerate polygons may not be displayed properly by graphics packages, but many
packages (including OpenGL) will not check for degenerate polygons as this takes extra
processing time which would slow down the rendering process. Therefore it is up to the
programmer to make sure that no degenerate polygons are specified.
How can we identify if a given polygon is convex? A number of approaches exist:
Compute and check all interior angles – if one is greater than 180o then the
polygon is concave; otherwise it is convex.
Infinitely extend each edge, and check for an intersection of the infinitely
extended edge with other edges in the polygon. If any intersections exist for any
edge, the polygon is concave; otherwise it is convex.
2.7. OpenGL Fill-Area Routines
OpenGL provides a variety of routines to draw fill-area polygons. In all cases these
polygons must be convex. In most cases the vertices should be specified in an anti-
clockwise direction when viewing the polygon from outside the object, i.e. if you want
the front-face to point towards you. The default fill-style is solid, in a colour determined
by the current colour settings.
In all, there are seven different ways to draw polygons in OpenGL:
glRect*
Six different symbolic constants for use with glBegin … glEnd
We will now consider each of these techniques in turn.
12 | P a g e Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
glRect*
Two-dimensional rectangles can also be drawn using some of the other techniques
described below, but because drawing rectangles in 2-D is a common task OpenGL
provides the glRect* routine especially for this purpose (glRect* is more efficient for 2-D
graphics than the other alternatives). The basic format of the routine is:
glRect* (x1, y1, x2, y2)
where (x1, y1) and (x2, y2) define opposite corners of the rectangle. Actually when we
call the glRect* routine, OpenGL will construct a polygon with vertices defined in the
following order:
(x1, y1), (x2, y1), (x2, y2), (x1, y2)
For example, Figure 12 shows an example of executing the following call to glRecti:
glRecti(200, 100, 50, 250);
In 2-D graphics we don’t need to worry about front and back faces – both faces will be
displayed. But if we use glRect* in 3-D graphics we must be careful. For example, in the
above example we actually specified the vertices in a clockwise order. This would mean
that the back-face would be facing toward the camera. To get an anti-clockwise order
(and the front-face pointing towards the camera), we must specify the bottom-left and
top-right corners in the call to glRect*.
Figure 12 - Drawing a 2-D Rectangle with the OpenGL routine glRect*
GL_POLYGON
The GL_POLYGON symbolic constant defines a single convex polygon. Like all of the
following techniques for drawing fill-area primitives it should be used as the argument to
the glBegin routine. For example, the code shown below will draw the shape shown in
Figure 13. Notice that the vertices of the polygon are specified in anti-clockwise order.
glBegin(GL_POLYGON);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glVertex2iv(p6);
glEnd( );
Figure 13 - A Polygon Drawn Using the GL_POLYGON OpenGL Primitive
13 | P a g e Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
GL_TRIANGLES
The GL_TRIANGLES symbolic constant causes the glBegin … glEnd pair to treat the
vertex list as groups of three 3 vertices, each of which defines a triangle. The vertices of
each triangle must be specified in anti-clockwise order. Figure 14 illustrates the use of the
GL_TRIANGLES primitive.
glBegin(GL_TRIANGLES);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p6);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd( );
Figure 14 - Triangles Drawn Using the GL_TRIANGLES OpenGL Primitive
GL_TRIANGLE_STRIP
To form polygonal meshes it is often convenient to define a number of triangles using a
single glBegin … glEnd pair. The GL_TRIANGLE_STRIP primitive enables us to define a
strip of connected triangles. 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.
glBegin(GL_TRIANGLE_STRIP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p6);
glVertex2iv(p3);
glVertex2iv(p5);
glVertex2iv(p4);
glEnd( );
Figure 15 - Triangles Drawn Using the GL_TRIANGLE_STRIP OpenGL Primitive
GL_TRIANGLE_FAN
GL_TRIANGLE_FAN is similar to GL_TRIANGLE_STRIP, in that it allows us to define a
number of triangles using a single glBegin … glEnd command. In this case, we can define
a ‘fan’ of triangles emanating from a single point. The first point of the first triangle is the
source of the fan, and the vertices of this triangle must be specified in an anti-clockwise
direction. 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.
glBegin(GL_TRIANGLE_FAN);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
14 | P a g e Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
glVertex2iv(p5);
glVertex2iv(p6);
glEnd( );
Figure 16 - Triangles Drawn Using the GL_TRIANGLE_FAN OpenGL Primitive
GL_QUADS
Using the GL_QUADS primitive, the vertex list is treated as groups of four vertices, each
of which forms a quadrilateral. If the number of vertices specified is not a multiple of
four, then the extra vertices are ignored. See Figure 17 for an example of GL_QUADS.
glBegin(GL_QUADS);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glVertex2iv(p6);
glVertex2iv(p7);
glVertex2iv(p8);
glEnd( );
Figure 17 - Quadrilaterals Drawn Using the GL_QUADS OpenGL Primitive
GL_QUAD_STRIP
In the same way that GL_TRIANGLE_STRIP allowed us to define a strip of connected
triangles, GL_QUAD_STRIP allows us to define a strip of quadrilaterals. The first four
vertices form the first quadrilateral, and each subsequent pair of vertices is combined
with the two before them to form another quadrilateral. The vertices of the first
quadrilateral must be specified in an anti-clockwise direction. Figure 18 illustrates the use
of GL_QUAD_STRIP.
glBegin(GL_QUAD_STRIP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glVertex2iv(p6);
glVertex2iv(p7);
glVertex2iv(p8);
glEnd( );
Figure 18 - Quadrilaterals Drawn Using the GL_QUAD_STRIP OpenGL Primitive
15 | P a g e Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
2.8. Character Primitives
The final type of graphics primitive we will consider is the character primitive. Character
primitives can be used to display text characters. Before we examine how to display
characters in OpenGL, let us consider some basic concepts about text characters.
We can identify two different types of representation for characters: bitmap and stroke
(or outline) representations. Using a bitmap representation (or font), characters are stored
as a grid of pixel values (see Figure 19(a)). This is a simple representation that allows fast
rendering of the character. However, such representations are not easily scalable: if we
want to draw a larger version of a character defined using a bitmap we will get an
aliasing effect (the edges of the characters will appear jagged due to the low resolution).
Similarly, we cannot easily generate bold or italic versions of the character. For this
reason, when using bitmap fonts we normally need to store multiple fonts to represent the
characters in different sizes/styles etc.
An alternative representation to bitmaps is the stroke, or outline, representation (see
Figure 19(b)). Using stroke representations characters are stored using line or curve
primitives. To draw the character we must convert these primitives into pixel values on
the display. As such, they are much more easily scalable: to generate a larger version of
the character we just multiply the coordinates of the line/curve primitives by some
scaling factor. Bold and italic characters can be generated using a similar approach. The
disadvantage of stroke fonts is that they take longer to draw than bitmap fonts.
We can also divide character primitives into serif and sans-serif fonts. Sans-serif fonts
have no accents on the characters, whereas serif fonts do have accents. For example, as
shown below, Arial and Verdana fonts are examples of sans-serif fonts whereas Times-
Roman and Garamond are serif fonts:
Arial is a sans-serif font
Verdana is a sans-serif font
Times Roman is a serif font
Garamond is a serif font
Finally, we can categorise fonts as either monospace or proportional fonts. Characters
drawn using a monospace font will always take up the same width on the display,
regardless of which character is being drawn. With proportional fonts, the width used on
the display will be proportional to the actual width of the character, e.g. the letter ‘i’ will
take up less width than the letter ‘w’. As shown below, Courier is an example of a
monospace font whereas Times-Roman and Arial are examples of proportional fonts:
Courier is a monospace font
Times-Roman is a proportional font
Arial is a proportional font
Figure 19 - Bitmap and Stroke Character
Primitives
(a) (b)
16 | P a g e Computer Graphics (SEng 4103)
DMIoT School of Computing Software Engineering Academic Program
OpenGL Character Primitive Routines
OpenGL on its own does not contain any routines dedicated to drawing text characters.
However, the glut library does contain two different routines for drawing individual
characters (not strings). Before drawing any character, we must first set the raster
position, i.e. where will the character be drawn. We need to do this only once for each
sequence of characters. After each character is drawn the raster position will be
automatically updated ready for drawing the next character. To set the raster position we
use the glRasterPos2i routine. For example,
glRasterPos2i(x, y)
positions the raster at coordinate location (x, y).
Next, we can display our characters. The routine we use to do this will depend on
whether we want to draw a bitmap or stroke character. For bitmap characters we can
write, for example,
glutBitmapCharacter(GLUT_BITMAP_9_BY_15, ‘a’);
This will draw the character ‘a’ in a monospace bitmap font with width 9 and height 15
pixels. There are a number of alternative symbolic constants that we can use in place of
GLUT_BITMAP_9_BY_15 to specify different types of bitmap font. For example,
GLUT_BITMAP_8_BY_13
GLUT_BITMAP_9_BY_15
We can specify proportional bitmap fonts using the following symbolic constants:
GLUT_BITMAP_TIMES_ROMAN_10
GLUT_BITMAP_HELVETICA_10
Here, the number represents the height of the font.
Alternatively, we can use stroke fonts using the glutStrokeCharacter routine. For
example,
glutStrokeCharacter(GLUT_STROKE_ROMAN, ‘a’);
This will draw the letter ‘a’ using the Roman stroke font, which is a proportional font.
We can specify a monospace stroke font using the following symbolic constant:
GLUT_STROKE_MONO_ROMAN
17 | P a g e Computer Graphics (SEng 4103)