Chapter 3 - CG (2024)
Chapter 3 - CG (2024)
Chapter 3 - CG (2024)
Faculty of Technology
Department of Computer Science
CoSc3072 – Computer Graphics
Chapter 3 Handout – Graphics Primitives
1. Introduction
All graphics packages construct pictures from basic building blocks known as graphics
primitives.
I. Geometric 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).
II. Fill Area Primitives: Refers to any enclosed boundary that can be filled with a
solid color or pattern.
III. Character Primitives: Used to display text characters
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.
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 2. 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();
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
Computer Graphics Page 1
(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 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 = (1/m).δy ............................................................................................................(5)
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.
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:
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.
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
We make this decision by first subtracting dupper from dlower:
dlower dupper 2mxk 1 2 yk 2c 1 .....................................................................(9)
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(dlower dupper ) 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.
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).
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
while 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
Plot (x2,y2) = (x1+1,y1) = (12,11)
p2 p1 2y 3 6 3
Iteration 2:
o p2 ≥ 0, so
Plot (x3,y3) = (x2+1,y2+1) = (13,12)
p3 p2 2y 2x 3 4 1
Iteration 3:
o p3 < 0, so
Plot (x4,y4) = (x3+1,y3) = (14,12)
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.
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_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
Where (xc,yc) is the centre of the circle. Alternatively, in polar coordinates we can write:
y yc r 2 xc x
2
…………………………………………………… (17)
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)
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
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
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:
In order to make this decision quickly and efficiently, we define a decision variable pk,
by combining Eqs. (18) and (19):
pk fcirc xk 1, yk 0.5 xk 1 y 0.5 r2
2 2
………………….. (20)
An incremental calculation for pk+1 can be derived by subtracting pk from pk+1 and
simplifying – the result is:
4
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 2xk 1 1
o Otherwise:
Plot (xk+1,yk-1)
pk 1 pk 2xk 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)
5. 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
color 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 8).
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.
(a) (b)
Figure 10 – Types of Polygon: (a) Convex; (b) Concave
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 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 11(a). Using this technique, we determine if a
point P is inside or outside the polygon boundary by the following steps:
The nonzero winding number rule is similar to the odd-even rule, and is illustrated in
Figure 11(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 11(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 11 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)
Figure 11 - Inside-Outside Tests for Polygons
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
Error! Reference source not found. 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.
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 color determined by
the current color 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:
For example, Figure 12 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 13. Notice that the vertices of the
polygon are specified in anti-clockwise order.
glBegin(GL_POLYGON);
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();
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 Figure 16 - Triangles Drawn Using the
list is treated as groups of four vertices, each GL_TRIANGLE_FAN OpenGL
of which forms a quadrilateral. If the number Primitive
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
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
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.
(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
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
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.
Triangle_Demo
150 100
150 150
100 100
200 100
300 100
260 200
150 300
200 400
100 300
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
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 5cos90o 0 , y 0 5sin 90 o 5 , so we plot (0,5)
o θ = 78.54o, x 0 5cos78.54 0.99 , y 0 5sin 78.54 o 4.9 , so we
o
plot (1,5)
o θ = 67.08o, x 0 5cos67.08 1.95, y 0 5sin 67.08 o 4.6 , so we
o
plot (2,5)
o θ = 55.62o, x 0 5cos55.62o 2.82 , y 0 5sin 55.62 o 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.