Chapter 2 - Graphics Primitive
Chapter 2 - Graphics Primitive
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 polyhedral (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.
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.
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:
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 end
y0
m …………………………………………………………… (2)
xend x0
c y0 mx0 …………………………………………………………………… (3)
Furthermore, for any given x-interval δx, we can calculate the corresponding y-interval δy:
These equations form the basis of the two line-drawing algorithms described below: the DDA
algorithm and Bresenham’s 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.
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
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:
δ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
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.
Bresenham’s algorithm works as follows. First, we denote by dupper and dlower the distances
between the centres of pixels A and B and the ‘true’ line (see Figure 3). Using Eq. (1) the ‘true’
y-coordinate at xk+1 can be calculated as:
y m( xk 1) c …………………………………………………………… (6)
Now, we can decide which of pixels A and B to choose based on comparing the values of dupper
and dlower:
If dlower > dupper, choose pixel A
Otherwise choose pixel B
If the value of this expression is positive we choose pixel A; otherwise we choose pixel B. The
question now is how we can compute this value efficiently. To do this, we define a decision
variable pk for the kth step in the algorithm and try to formulate pk so that it can be computed
using only integer operations. To achieve this, we substitute m y / x (where Δx and Δy are
the horizontal and vertical separations of the two line end-points) and define pk as:
pk x(d lower d upper ) 2yxk 2xyk d …………………………… (10)
Where d is a constant that has the value 2y 2cx x . Note that the sign of pk will be the
same as the sign of (dlower – dupper), so if pk is positive we choose pixel A and if it is negative we
choose pixel B. In addition, pk can be computed using only integer calculations, making the
algorithm very fast compared to the DDA algorithm.
An efficient incremental calculation makes Bresenham’s algorithm even faster. (An incremental
calculation means we calculate the next value of pk from the last one.) Given that we know a
value for pk, we can calculate pk+1 from Eq. (10) by observing that:
Always xk+1 = xk+1
If pk < 0, then yk+1 = yk, otherwise yk+1 = yk+1
The initial value for the decision variable, p0, is calculated by substituting xk = x0 and yk = y0 into
Eq. (10), which gives the following simple expression:
So we can see that we never need to compute the value of the constant d in Eq. (10).
Summary
The steps given above will work for lines with positive |m| < 1. For |m| > 1 we simply swap the
roles of x and y. For negative slopes one coordinate decreases at each iteration whilst the other
increases.
Exercise
Consider the example of plotting the line shown in Figure 2 using Bresenham’s algorithm:
First, compute the following values:
o Δx = 5
o Δy = 3
o 2Δy = 6
o 2Δy - 2Δx = -4
o p0 2y x 2 3 5 1
Plot (x0,y0) = (10,10)
Iteration 0:
o p0 ≥ 0, so
Plot (x1,y1) = (x0+1,y0+1) = (11,11)
p1 p0 2y 2x 1 4 3
Iteration 1:
o p1 < 0, so
We can see that the algorithm plots exactly the same points as the DDA algorithm but it
computes those using only integer operations. For this reason, Bresenham’s algorithm is the most
popular choice for line-drawing in computer graphics.
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.
glBegin(GL_LINES);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
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.
4. Circle-Drawing Algorithms
Some graphics packages allow us to draw circle primitives. 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 …………………………………………………… (14)
where (xc,yc) is the centre of the circle. Alternatively, in polar coordinates we can write:
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.
y yc r 2 xc x
2
…………………………………………………… (17)
This would correctly generate points on the boundary of a circle. However, like the first attempt
at a line-drawing algorithm we saw in Section 3.1 (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, 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. (17), would
make the algorithm quite inefficient.
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. (15) and (16).
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)
Increase θ to 10o:
x = 5 + 10 cos 10o = 14.85
y = 5 + 10 sin 10o = 6.73
Therefore we plot (15,7)
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 described in Section 4.1. 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).
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 described in Section 4.1. 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.
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).
It would be nice if there were an algorithm for circle-drawing that used integer operations only,
in the same way that Bresenham’s algorithm does for line-drawing.
The midpoint algorithm takes advantage of the symmetry property of circles to produce a more
efficient algorithm for drawing circles. The algorithm works in a similar way to Bresenham’s
line-drawing algorithm, in that it formulates a decision variable that can be computed using
integer operations only.
The midpoint algorithm is illustrated in Figure 7. Recall that we only need to draw one octant of
the circle, as the other seven octants can be generated by symmetry. In Figure 7 we are drawing
the right-hand upper octant – the one with coordinate (y,x) in Figure 6, but the midpoint
algorithm would work with slight modifications whichever octant we chose. Notice that in this
octant, when we move from one pixel to try to draw the next pixel there is only a choice of two
pixels: A and B, or (xk+1,yk) and (xk+1,yk-1). Therefore we don’t need to calculate a real-valued
coordinate and then round to the nearest pixel, we just need to make a decision as to which of the
two pixels to choose.
This term can be derived directly from Eq. (14). Based on the result of this function, we can
determine the position of any point relative to the circle boundary:
For points on circle, fcirc= 0
For points inside circle, fcirc< 0
For points outside circle, fcirc> 0
Now referring again to Figure 7, we note that the position of the midpoint of the two pixels A and
B can be written as:
Now we can see from Figure 7 that if the midpoint lies inside the circle boundary the next pixel
to be plotted should be pixel A. Otherwise it should be pixel B. Therefore we can use the value of
fcirc(midk) to make the decision between the two candidate pixels:
If fcirc(midk) < 0, choose A
Otherwise choose B
In order to make this decision quickly and efficiently, we define a decision variable pk, by
combining Eqs. (18) and (19):
An incremental calculation for pk+1 can be derived by subtracting pk from pk+1 and simplifying –
the result is (Therefore we can define the incremental calculation as):
If r is an integer, then all increments are integers and we can round Eq. (23) to the nearest
integer:
p0 = 1 – r …………………………………………………………………… (24)
Summary
To summarise, we can express the midpoint circle-drawing algorithm for a circle centred at the
origin as follows:
Plot the start-point of the circle (x0,y0) = (0,r)
Compute the first decision variable:
o p0 1 r
For each k, starting with k=0:
o If pk < 0:
Plot (xk+1,yk)
pk 1 pk 2 xk 1 1
o Otherwise:
Plot (xk+1,yk-1)
pk 1 pk 2 xk 1 1 2 yk 1
Example
For example, given a circle of radius r=10, centred at the origin, the steps are:
First, compute the initial decision variable:
o p0 1 r 9
Plot (x0,y0) = (0,r) = (0,10)
Iteration 0:
o p0 < 0, so
Plot (x1,y1) = (x0+1,y0) = (1,10)
p1 p0 2 x1 1 9 3 6
Iteration 1:
o p1 < 0, so
Plot (x2,y2) = (x1+1,y1) = (2,10)
p2 p1 2 x2 1 6 5 1
Iteration 2:
o p2 < 0, so
Plot (x3,y3) = (x2+1,y2) = (3,10)
p3 p 2 2 x3 1 1 7 6
Iteration 3:
o p3 ≥ 0, so
Plot (x4,y4) = (x3+1,y3-1) = (4,9)
p 4 p3 2 x 4 1 2 y 4 6 9 3
Iteration 4:
o p4 < 0, so
Plot (x5,y5) = (x4+1,y4) = (5,9)
p5 p 4 2 x5 1 3 11 8
Iteration 5:
o p5 ≥ 0, so
Plot (x6,y6) = (x5+1,y5-1) = (6,8)
p 6 p5 2 x6 1 2 y 6 8 3 5
Etc.
5. Ellipse-Drawing Algorithms
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 …………………………………………………… (25)
rx ry
To understand these equations, refer to Figure 8. Notice that Eqs. (25)-(27) are similar to the
circle equations given in Eqs. (14)-(16), except that ellipses have two radius parameters: rx and
ry. 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.
The most efficient algorithm for drawing ellipses is an adaptation of the midpoint algorithm for
circle-drawing. Recall that in the midpoint algorithm for circles we first had to define a term
whose sign indicates whether a point is inside or outside the circle boundary, and then we
applied it to the midpoint of the two candidate pixels. For ellipses, we define the function fellipse
as:
Eq. (28) can be derived from Eq. (25) by assuming that (xc,yc) = (0,0), and then multiplying both
sides by rx2 ry2 . Now we can say the same for fellipse as we did for the circle function:
For points on ellipse fellipse= 0
For points inside ellipse fellipse< 0
For points outside ellipse fellipse> 0
But at this point we have a slight problem. For circles there was 4-way symmetry so we only
needed to draw a single octant. This meant we always had a choice of just 2 pixels to plot at each
iteration. But ellipses only have 2-way symmetry, which means we have to draw an entire
quadrant (see Figure 9).
Again, a fast incremental algorithm exists to compute a decision function, but because of the
gradient calculation the midpoint ellipse-drawing algorithm is not as efficient as the midpoint
circle drawing algorithm.
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 10).
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 11.
(a) (b)
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.
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.
Compute the cross-product of each pair of adjacent edges. If all cross-products have the
same sign for their z-coordinate (i.e. they point in the same direction away from the plane
of the polygon), then the polygon is convex; otherwise it is concave.
The last technique used the cross-product of vectors. The result of the cross-product of two
vectors is a vector perpendicular to both vectors, whose magnitude is the product of the two
vector magnitudes multiplied by the sin of the angle between them:
N u E1 E2 sin , where 0 ≤ θ ≤ 180o …………………………………… (29)
Because most graphics packages insist on polygons being convex, once we have identified
concave polygons we need to split them up to create a number of convex polygons.
One technique for splitting concave polygons is known as the vector technique. This is illustrated
in Figure 14, and can be summarised as follows:
Compute the cross-product of each pair of adjacent edge vectors.
When the cross-product of one pair has a different sign for its z-coordinate compared to
the others (i.e. it points in the opposite direction):
o Extend the first edge of the pair until it intersects with another edge.
o Form a new vertex at the intersection point and split the polygon into two.
Recall from Section 6.1 that the cross-product switches direction when the angle between the
vectors becomes greater than 180o. Therefore the vector technique is detecting this condition by
using a cross-product.
In order to fill polygons we need some way of telling if a given point is inside or outside the
polygon boundary: we call this an inside-outside test. We will see in Chapter 7 that such tests are
also useful for the ray-tracing rendering algorithm.
We will examine two different inside-outside tests: the odd-even rule and the nonzero winding
number rule. Both techniques give good results, and in fact usually their results are the same,
apart from for some more complex polygons.
The odd-even rule is illustrated in Figure 15(a). Using this technique, we determine if a point P
is inside or outside the polygon boundary by the following steps:
Draw a line from P to some distant point (that is known to be outside the polygon
boundary).
Count the number of crossings of this line with the polygon boundary:
o If the number of crossings is odd, then P is inside the polygon boundary.
o If the number of crossings is even, then P is outside the polygon boundary.
We can see from Figure 15(a) that the two white regions are considered to be outside the
polygon since they have two line crossings to any distant points.
The nonzero winding number rule is similar to the odd-even rule, and is illustrated in Figure
15(b). This time we consider each edge of the polygon to be a vector, i.e. they have a direction as
well as a position. These vectors are directed in a particular order around the boundary of the
polygon (the programmer defines which direction the vectors go). Now we decide if a point P is
inside or outside the boundary as follows:
Draw a line from P to some distant point (that is known to be outside the polygon
boundary).
At each edge crossing, add 1 to the winding number if the edge goes from right to left,
and subtract 1 if it goes from left to right.
o If the total winding number is nonzero, P is inside the polygon boundary.
o If the total winding number is zero, P is outside the polygon boundary.
We can see from Figure 15(b) that the nonzero winding number rule gives a slightly different
result from the odd-even rule for the example polygon given. In fact, for most polygons
(including all convex polygons) the two algorithms give the same result. But for more complex
polygons such as that shown in Figure 15 the nonzero winding number rule allows the
programmer a bit more flexibility, since they can control the order of the edge vectors to get the
results they want.
(a) (b)
Polygons, and in particular convex polygons, are the most common type of primitive in 3-D
graphics because they are used to represent polygonal meshes such as those shown in Figure 11.
But how can polygonal meshes be represented? A common technique is to use tables of data.
These tables can be of two different types:
Geometric tables: These store information about the geometry of the polygonal mesh, i.e.
what are the shapes/positions of the polygons?
Attribute tables: These store information about the appearance of the polygonal mesh, i.e.
what colour is it, is it opaque or transparent, etc. This information can be specified for
each polygon individually or for the mesh as a whole.
Figure 16 shows a simple example of a geometric table. We can see that there are three tables: a
vertex table, an edge table and a surface-facet table. The edge table has pointers into the vertex
table to indicate which vertices comprise the edge. Similarly the surface-facet table has pointers
into the edge table. This is a compact representation for a polygonal mesh, because each vertex’s
coordinates are only stored once, in the vertex table, and also information about each edge is
only stored once.
Most polygons are part of a 3-D polygonal mesh, which are often enclosed (solid) objects.
Therefore when storing polygon information we need to know which face of the polygon is
facing outward from the object. Every polygon has two faces: the front-face and the back-face.
The front-face of a polygon is defined as the one that points outward from the object, whereas
the back-face points towards the object interior.
Often in graphics we need to decide if a given point is on one side of a polygon or the other. For
example, if we know the position of the virtual camera we may want to decide if the camera is
looking at the front-face or the back-face of the polygon. (It can be more efficient for graphics
packages not to render back-faces.)
Ax + By + Cz + D = 0 …………………………………………………… (30)
Given at least three coplanar points (e.g. polygon vertices) we can always calculate the values of
the coefficients A, B, C, and D for a plane. Now, for any given point (x,y,z):
If Ax + By + Cz + D = 0, the point is on the plane.
If Ax + By + Cz + D < 0, the point is behind the plane.
If Ax + By + Cz + D > 0, the point is in front of the plane.
Polygon front-faces are usually identified using normal vectors. A normal vector is a vector that
is perpendicular to the plane of the polygon and which points away from front face (see Figure
17).
In graphics packages, we can either specify the normal vectors ourselves, or get the package to
compute them automatically. How will a graphics package compute normal vectors
automatically?
The simplest approach is to use the cross-product of adjacent edge vectors in the polygon
boundary. Recall that the result of a cross-product is a vector that is perpendicular to the two
vectors. Consider Figure 18. Here we can see three vertices of a polygon: V1, V2 and V3. These
form the two adjacent edge vectors E1 and E2:
E1 = (1,0,0)
E2 = (0,1,0)
Now, using Eq. (29) we can compute the surface normal N u E1 E2 sin , where 0 ≤ θ ≤ 180o,
and the direction of u is determined by the right-hand rule. This will give a vector that is
perpendicular to the plane of the polygon. In this example, N (0,0,1) .
Notice that if we consider the edge vectors to go the other way round the boundary (i.e.
clockwise instead of anti-clockwise) then the normal vector would point in the other direction.
This is the reason why in most graphics packages it is important which order we specify our
polygon vertices in: specifying them in an anti-clockwise direction will make the normal vector
point towards us, whereas specifying them in a clockwise direction will make it point away from
us.
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.
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:
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:
For example, Figure 19 shows an example of executing the following call to glRecti:
glRecti(200,100,50,250);
(The black crosses are only shown for the purpose of illustrating where the opposing corners of
the rectangle are.)
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*.
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 20.
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();
glBegin(GL_TRIANGLES);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p6);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glEnd();
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. The vertices of the first triangle only must be specified in anti-clockwise
order. Figure 22 illustrates the use of GL_TRIANGLE_STRIP.
glBegin(GL_TRIANGLE_STRIP);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p6);
glVertex2iv(p3);
glVertex2iv(p5);
glVertex2iv(p4);
glEnd();
GL_TRIANGLE_FAN
glBegin(GL_TRIANGLE_FAN);
glVertex2iv(p1);
glVertex2iv(p2);
glVertex2iv(p3);
glVertex2iv(p4);
glVertex2iv(p5);
glVertex2iv(p6);
glEnd();
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. The vertices for each quadrilateral must be defined in an anti-
clockwise direction. See Figure 24 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 24 - 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 25 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 25 - Quadrilaterals Drawn Using the
GL_QUAD_STRIP OpenGL Primitive
7. 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 26(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.
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:
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:
(a) (b)
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)
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
Summary
Exercises
1) The figure below shows the start and end points of a straight line.
a. Show how the DDA algorithm would draw the line between the two points.
b. Show how Bresenham’s algorithm would draw the line between the two points.
2) Show how the following circle-drawing algorithms would draw a circle of radius 5 centred
on the origin. You need only consider the upper-right octant of the circle, i.e. the arc shown
in red in the figure below.
3) Show which parts of the fill-area primitive shown below would be classified as inside or
outside using the following inside-outside tests:
a. Odd-even rule.
4) Write a C++/OpenGL program to read in a string and a sequence of points from a file, and
display the string as a title and the points as triangles using the GL_TRIANGLES
primitive. For example, the file listed below would lead to the following image being
displayed.
Triangle_Demo
150 100
150 150
100 100
200 100
300 100
260 200
150 300
200 400
100 300
Exercise Solutions
Using these values of δx and δy we can now start to plot line points:
o Start with (x0,y0) = (0,0) – colour this pixel
o Next, (x1,y1) = (0+1,0+0.67) = (1,0.67) – so we colour pixel (1,1)
o Next, (x2,y2) = (1+1,1 +0.67) = (2,1.33) – so we colour pixel (2,1)
o Next, (x3,y3) = (2+1,1 +0.67) = (3,2) – so we colour pixel (3,2)
o We have now reached the end-point (xend,yend), so the algorithm terminates
a. We start from x = 0, and then successively increment xand calculate the corresponding
y using Eq. (17): y yc r 2 xc x . We can tell that we have left the first octant
2
when x > y.
o x = 0, y 0 5 2 0 0 5 , so we plot (0,5).
2
o x = 3, y 0 5 2 0 3 4 , so we plot (3,4).
2
The figure below shows the first quadrant of the circle, with the plotted points shown in
blue, and points generated by symmetry in grey.
b. First we calculate the angular increment using θ = 1/r radians. This is equal to
(1/5)*(180/π) = 11.46o. Then we start with θ = 90o, and compute x and y according to
Eqs. (15) and (16) for successive value of θ, subtracting 11.46o at each iteration. We
stop when θ becomes less than 45o.
o θ = 90o, x 0 5 cos 90o 0 , y 0 5 sin 90o 5 , so we plot (0,5)
o θ = 78.54o, x 0 5 cos 78.54o 0.99 , y 0 5 sin 78.54o 4.9 , so we plot
(1,5)
o θ = 67.08o, x 0 5 cos 67.08o 1.95 , y 0 5 sin 67.08o 4.6 , so we plot (2,5)
o θ = 55.62o, x 0 5 cos 55.62o 2.82 , y 0 5 sin 55.62o 4.13 , so we plot
(3,4)
o θ = 44.16o, which is less than 45o, so we stop.
The points plotted are the same as for the Cartesian plotting algorithm.
3) The figure below shows the classifications for each of the algorithms: the grey shaded areas
are classified as inside, and the white areas as outside. In this case the two algorithms
produce different results. The odd-even rule classifies the inner polygon as outside because
points inside it have two (an even number) line crossings to reach any distant point. For the
nonzero winding number rule, both of the line crossings go from right to left, so the
winding number is incremented in both cases. Therefore the total winding number for
points inside the inner polygon is 2, which in nonzero and so the points are classified as
inside. By reversing the direction of the edge vectors of the inner (or outer) polygon we
could get the same result as the odd-even rule.
4) See the code listing included in the zip file for this handout for the solution to this question.