All Basics of Computer Graphics
All Basics of Computer Graphics
Computer Graphics
Part I:- Introduction to Computer Graphics
The aim of computer graphics is to produce realistic and/or useful images on a computer. For some
applications the emphasis will be on realism (e.g. special effects in films), whereas for others it
will simply be on usefulness (e.g. data visualisation). We will discuss some different applications
of computer graphics in Section 4.
Computer graphics provides methods to generate images using a computer. The word “image”
should be understood in a more abstract sense here. An image can represent a realistic scene from
the real world, but graphics like histograms (statics diagram) or pie charts as well as the graphical
user interface of a software tool are also considered as images. The development of computer
graphics has made computers easier to interact with, and better for understanding and interpreting
many types of data. Developments in computer graphics have had a profound impact on many
types of media and have revolutionized animation, movies and the video game industry.
The term computer graphics has been used in a broad sense to describe "almost everything on
computers that is not text or sound". Typically, the term computer graphics refers to several
different things:
Today, computers and computer-generated images touch many aspects of daily life. Computer
imagery is found on television, in newspapers, for example in weather reports, or for example in
all kinds of medical investigation and surgical procedures. A well-constructed graph can present
complex statistics in a form that is easier to understand and interpret. In the media "such graphs
are used to illustrate papers, reports, thesis", and other presentation material.
Many powerful tools have been developed to visualize data. Computer generated imagery can be
categorized into several different types: 2D, 3D, 4D, 7D, and animated graphics. As technology
has improved, 3D computer graphics have become more common, but 2D computer graphics are
still widely used. Computer graphics has emerged as a sub-field of computer science which studies
Whatever the application, the final image will be generated from some model of the scene we want
to take a picture of. For example, in Figure 1 we have a representation of a 3-D tetrahedron (the
object model), together with some model of how the scene is lit (the light sources), and we want
to produce an image on the image plane based on the position of the viewer. The object model will
typically represent the geometry of the object together with some information about what type of
material it is made of (i.e. what it looks like). Normally we consider the viewer to be a virtual
camera, which defines how the picture is taken. This process of starting with a model (often a 3-
D model) of the world and taking a picture of it with a virtual camera is known in computer
graphics as rendering. The rendering process can include many stages, such as hidden surface
elimination, surface rendering and clipping, which we will deal with throughout this course.
Some graphics applications can be described as interactive graphics applications. In these cases,
images need to be generated in real-time and as well as viewing the images the user can interact
with the model of the world using specialised input hardware. For example, a mouse, keyboard,
tablet and stylus, scanner or a virtual reality headset and gloves can all be used as interactive
graphics input devices.
The advance in computer graphics was to come from one MIT (Massachusetts Institute of
Technology: an engineering university in Cambridge) student, Ivan Sutherland. In 1961 Sutherland
created another computer drawing program called Sketchpad. Using a light pen, Sketchpad
allowed one to draw simple shapes on the computer screen, save them and even recall them later.
Before we look in any more detail at how computers can generate graphical images, let us consider
why we would want to do this. Since the early days of computing, the field of computer graphics
has become a very popular one because of its wide range of applications. The following sections
summarise the main categories of application.
Graphs and charts have long been used for visualising data. They are particularly widely used for
visualising relationships and trends in scientific, mathematical, financial and economic data.
Although, in principle, we do not need advanced graphics algorithms to display graphs and charts,
many modern visualisation packages use 3-D effects such as shadowing to make the graph/chart
more visually appealing. For example, Figure 2 shows a range of graphs and charts produced by
the free FusionCharts Lite package (http://www.infosoftglobal.com/FusionCharts/Lite).
Commercial packages are also available and include Microsoft Excel.
(a) (b)
Computer-Aided Design (CAD) and Computer-Aided Drafting and Design (CADD) involve using
a computer application to enable designers to construct and visualise 3-D models. They are
commonly used in fields such as architecture, engineering and circuit design. For example, Figure
3 shows two screenshots from CAD applications being used for architecture. One of the most
common CAD applications is AutoCAD.
Computer-Aided Manufacturing (CAM) is a similar concept to CAD, except that the design
application is linked to the manufacturing process, i.e. the application will directly control, via a
hardware communication link, the machine that manufactures the object being designed.
(a) (b)
We already saw in Section 2.1 how graphics can be used for visualising data in the form of graphs
and charts. More complex, often multidimensional, datasets can also be visualised using graphics
techniques. In this case, it is often difficult to visualise such datasets without computer graphics.
These complex datasets are particularly common in science (scientific visualisation) and business
(business visualisation).
For example, Figure 4(a) shows a three-dimensional medical dataset of a patient. Here we use
computer graphics to visualise the paths of blood vessels inside the patient’s head. Figure 4(b)
shows an image generated from data acquired by the Hubble Space Telescope. This image of a
distant galaxy is allowing us to visualise a four-dimensional dataset – three dimensions are
combined as the red, green and blue components of an optical image, and the fourth (an X-ray
image) is displayed as overlaid contour plots. Figure 4(c) shows another medical dataset, this time
acquired using a Magnetic Resonance Imaging (MRI) scanner. Computer graphics allow us to
visualise this three-dimensional dataset as three slices through the 3-D volume. Finally, Figure
4(d) shows the use of colour to visualise altitude in a map of the United States.
(a) (b)
Virtual Reality (VR) allows users to be immersed in a computer generated world, and to interact
with it as if they were interacting with the real world. Typically, the user will wear special hardware
such as the VR-headset shown in Figure 5(a). This allows the computer to completely control the
visual information received by the user, so if realistic computer-generated images such as that
shown in Figure 5(b) are displayed in the headset in stereo (i.e. different images for the left and
right eyes) the user will have a sense of physical immersion in the virtual world. In addition, other
specialised hardware such as VR-gloves can be used to interact with the ‘objects’ in the virtual
world.
An alternative way of experiencing virtual reality is by using a CAVE (which stands for Cave
Automatic Virtual Environment). An example of this is shown in Figure 5(c). Here, the user stands
inside a cubicle which has images projected onto the walls and ceiling. Often there will also be a
means of ‘moving’ in the virtual world such as pressure pads.
Augmented Reality (AR) combines a computer-generated virtual world with the real world. In AR,
computer-generated images are overlaid onto a user’s view of the real world. For example, Figure
5(d) shows an AR system used for surgery, in which computer-generated images of hidden features
such as blood vessels and tumours are overlaid on the surgeon’s view of the patient through a
surgical microscope.
(c) (d)
Figure 5 - Computer Graphics used for Virtual Reality and Augmented Reality
Computer graphics can be very beneficial in education and training. For example, Figure 6(a)
shows a screenshot from some educational software that teaches students about human anatomy.
Graphics are used to help students visualise the appearance and location of different organs inside
the body.
Another common application of computer graphics is in training. In many jobs it is difficult for
workers to get direct experience of the environment that they are expected to work in. This may
be because the environment is inaccessible (e.g. space), dangerous (e.g. bomb disposal), expensive
(e.g. flying aircraft) or high-risk (e.g. surgery). Computer graphics, combined with specialised
hardware, can be used to simulate the working environment, so that workers can gain the skills
required for their work. For example, Figure 6(b) shows a picture of a truck-driving simulator used
(a) (b)
(c) (d)
Another possibility that has been opened up by the improvements in computer graphics technology
is in the area of user interfaces. Figure 7 shows the difference in visual appeal between an old text-
based user interface and a modern windows-based user interface. And the difference is not just in
visual appeal: modern user interfaces have made multitasking (doing several things at once) much
easier and applications such as word-processors have become much more powerful.
2.7. Entertainment
The applications of computer graphics in entertainment fall into three categories: computer art,
special effects and animations, and games.
A number of artists have taken advantage of the possibilities offered by computer graphics in
producing works of art. For example, Figure 8(a) shows a frame from an animated artwork by
William Latham.
Computer-generated images (CGI) have been widely used in the entertainment industry for
producing special effects for films, and also for producing completely computer-generated films.
Figure 8(b) shows a frame from the film Final Fantasy: the Spirits Within, which featured 100%
computer-generated images. For applications such as this, highly realistic images can be produced
off-line, i.e. we do not need to generate the images in real-time, they need only be generated once,
frame-by-frame, and then combined into the desired animated sequence.
For computer games, images must be generated in real-time in response to user actions. Therefore
the graphics in games can be less realistic, since the emphasis is on speed of generation rather than
realism. Figure 8(c) and (d) show screenshots from the popular computer game Doom.
(c) (d)
3. Graphics Software
To generate graphical images like those we have seen above, we need some type of graphics
software. We can divide graphics software into two categories:
Special-purpose graphics packages
General-purpose graphics packages
Special-purpose packages are designed for a specific purpose. For example a drawing package
such as the Paint accessory in Microsoft Windows is an example of a special-purpose graphics
package: it is designed to allow users to draw simple pictures consisting of lines, curves and other
basic components. Another example is CAD packages such as AutoCAD: these are designed to
allow users to build and visualise 3-D models. Special-purpose graphics packages tend to be used
mostly by end-users, not programmers or computer professionals. In other words, you do not need
General-purpose packages are a pre-defined library of routines for drawing graphics primitives.
They are used by computer programmers to add graphical capability to their programs. They
provide a lot more flexibility than special-purpose packages but they require technical knowledge
of computer graphics and programming and so are not suitable for end-users. Examples of general-
purpose graphics packages are the standard low-level graphics library OpenGL, the Microsoft
equivalent DirectX and the higher-level library OpenInventor. We can also think of scene
description languages as a type of general-purpose graphics package. Scene description languages
are languages that can be used to define 3-D models of scenes. Special viewing software is required
to view and often interact with the scene. The most common example of a scene description
language is VRML, the Virtual Reality Modelling Language.
To achieve a colour display, CRT devices have three electron beams, and on the screen the
phosphor dots are in groups of three, which give off red, green and blue light respectively. Because
the three dots are very close together the light given off by the phosphor dots is combined, and the
relative brightnesses of the red, green and blue components determines the colour of light
perceived at that point in the display.
Random scan displays are not so common for CRT devices, although some early video games did
use random scan technology. These days random scan is only really used by some hard-copy
plotters.
Raster scan CRT devices are the most common type of graphics display device in use today,
although recently LCD (Liquid Crystal Display) devices have been growing in popularity. In this
course, though, we will concentrate on the details of raster scan devices.
The number of times that the entire raster is refreshed (i.e. drawn) each second is known as the
refresh rate of the device. For the display to appear persistent and not to flicker the display must
update often enough so that we cannot perceive a gap between frames. In other words, we must
refresh the raster when the persistence of the phosphor dots is beginning to wear off. In practise,
if the refresh rate is more than 24 frames per second (f/s) the display will appear reasonably smooth
and persistent.
The following are the specifications of some common video formats that have been (and still are)
used in computer graphics:
VGA: resolution 640x480, 60 f/s refresh rate, non-interlaced scanning.
PAL: resolution 625x480, 25 f/s refresh rate, interlaced scanning
NTSC: resolution 525x480, 30 f/s refresh rate, interlaced scanning
The display processor is also known by a variety of other names: graphics controller, display
coprocessor, graphics accelerator and video card are all terms used to refer to the display
processor. Since the display processor contains dedicated hardware for executing graphics routines
it must be dedicated to a particular set of routines. In other words, display processors will only be
able to handle the graphics processing if the routines used are from a particular graphics package.
This is known as hardware rendering. Most commercial video cards will support hardware
rendering for the OpenGL graphics package, and many PC video cards will also support hardware
rendering for DirectX. Hardware rendering is much faster than the alternative, software rendering,
in which graphics routines are compiled and executed by the CPU just like any other code. For the
raster graphics architecture to support software rendering the block-diagram shown in Figure 14
would need to be modified so that the frame buffer was connected directly to the system bus in
order that it could be updated by the CPU.
In recent years the popularity of 3-D graphics display devices has been growing, although currently
they are still quite expensive compared with traditional 2-D displays. The aim of 3-D display
devices is to provide a stereo pair of images, one to each eye of the viewer, so that the viewer can
perceive the depth of objects in the scene as well as their position. The process of generating such
3-D displays is known as stereoscopy.
3-D displays can be divided into two types: head-mounted displays (HMDs) and head-tracked
displays (HTDs). HMDs are displays that are mounted on the head of the viewer. For example,
Figure 15 shows a HMD – the device fits on the head of the user and displays separate images to
the left and right eyes, producing a sense of stereo immersion. Such devices are common in virtual
reality applications. Normally a tracking device will be used to track the location of the display
device so that the images presented to the user can be updated accordingly – giving the impression
that the viewer is able to ‘move’ through the virtual world.
Whereas with a HMD the display moves with the viewers head, with a HTD the display remains
stationary, but the head of the viewer is tracked so that the images presented in the display can be
updated. The difficulty with HTDs is how to ensure that the left and right eyes of the viewer receive
separate stereo images to give a 3-D depth effect. A number of technologies exist to achieve this
aim. For example, the display can use polarised light filters to give off alternate vertically and
horizontally polarised images; the viewer then wears special glasses with polarised filters to ensure
that, for example, the left eye receives the vertically polarised images and the right eye receives
the horizontally polarised images. An alternative technique is to use colour filters: the display
draws a left-eye image in, for example, blue, and a right eye image in green; then the viewer wears
glasses with colour filters to achieve the stereo effect. Other technologies also exist.
5. OpenGL
Now we will introduce the OpenGL graphics package. This part of the course is aimed to give you
practical experience to reinforce the theory behind computer graphics. Code examples will be
In this course we will be using OpenGL together with the GL Utilities Toolkit (glut). You should
make sure that you have access to a C++ development environment that supports these two
libraries. Note that Turbo C++ does not support either of them. Two possibilities are:
The free Dev-C++ environment (www.bloodshed.net) has OpenGL built-in and glut can
be easily added on as a separate package.
Microsoft Visual C++ also has OpenGL built-in but not glut. The glut library is available
as a free download from http://www.xmission.com/~nate/glut.html. Installation is fairly
straightforward.
The examples and exercise solutions given in this course were coded with Dev-C++, but the same
source code should compile with Visual C++. Also, if you use Dev-C++, it comes with a very
useful OpenGL help file that you can use as a reference for different OpenGL routines (it will be
found in the Docs/OpenGL folder inside the Dev-C++ installation folder).
5.1. Introduction
OpenGL is based on the GL graphics package developed by the graphics hardware manufacturer
Silicon Graphics. GL was a popular package but it was specific to Silicon Graphics systems, i.e.
code written using GL would only run on Silicon Graphics hardware. In an attempt to overcome
this limitation, OpenGL was developed in the early 1990’s as a free platform-independent version
of GL. The package was developed, and is still maintained, by a consortium of graphics companies.
Although OpenGL is the core library that contains the majority of the functionality, there are a
number of associated libraries that are also useful. The following is a summary of the most
important of the OpenGL family of graphics libraries:
OpenGL: the core library, it is platform (i.e. hardware system) independent, but not
windows-system independent (i.e. the code for running on Microsoft Windows will be
different to the code for running on the UNIX environments X-Windows or Gnome).
glut: The GL Utilities Toolkit, it contains some extra routines for drawing 3-D objects and
other primitives. Using glut with OpenGL enables us to write windows-system independent
code.
glu: The OpenGL Utilities, it contains some extra routines for projections and rendering
complex 3-D objects.
glui: Contains some extra routines for creating user-interfaces.
Every routine provided by OpenGL or one of the associated libraries listed above follows the same
basic rule of syntax:
The prefix of the function name is either gl, glu, or glut, depending on which of these three
libraries the routine is from.
Some function arguments can be supplied as predefined symbolic constants. (These are basically
identifiers that have been defined using the C++ #define statement.) These symbolic constants are
always in capital letters, and have the same prefix convention as function names. For example,
GL_RGB, GL_POLYGON and GLUT_SINGLE are all symbolic constants used by OpenGL and
its associated libraries.
Finally, OpenGL has a number of built-in data types to help make it into a platform-independent
package. For example, the C++ int data type may be stored as a 16-bit number on some platforms
but as a 32-bit number on others. Therefore if we use these standard C++ data types with OpenGL
routines the resultant code will not be platform-independent. OpenGL provides its own data types
to overcome this limitation. Mostly, they have the same names as C++ data types but with the
prefix GL attached. For example, GLshort, GLint, GLfloat and GLdouble are all built-in OpenGL
data types. Although you will find that your OpenGL code will still work on your computer if you
do not use these data types, it will not be as portable to other platforms so it is recommended that
you do use them.
The first thing we need to know to be able to start writing OpenGL programs is which header files
to include. Exactly which header files are required depends upon which library(s) are being used.
For example,
If we are only using the core OpenGL library, then the following line must be added near
the beginning of your source code:
#include <GL/gl.h>
If we also want to use the GL Utilities library, we must add the following line:
#include <GL/glu.h>
If we want to use the glut library (and this makes using OpenGL a lot easier) we do not need to
include the OpenGL or glu header files. All we need to do is include a single header file for glut:
As well as including the appropriate header files, we should also link in the appropriate libraries
when compiling. For Dev-C++, all you need to do is select Multimedia/glut as the project type
when creating your new project – the rest will be done for you.
Now we are ready to start writing OpenGL code. Refer to the code listing in Appendix A for the
following discussion. Before displaying any graphics primitives we always need to perform some
basic initialisation tasks, including creating a window for display. The following lines initialise
the glut library, define a frame buffer and create a window for display.
glutInit(&argc, argv);
glutInitWindowSize(640,480);
glutInitWindowPosition(10,10);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glutCreateWindow(“GLUT Points demo”);
The most important event is the display event. This occurs whenever OpenGL decides that it needs
to redraw the display window. It is guaranteed to happen once at the beginning of the event loop;
after this it will occur whenever the window needs to be redrawn, for example if it is hidden by
another window and then uncovered. Any primitives that we want to draw should be drawn in the
display event callback function; otherwise they will not be displayed.
To specify a callback function we must first write a programmer-defined function, and then specify
that this function should be associated with a particular event. For example, the following code
specifies that the programmer-defined function myDisplay will be executed in response to the
display event:
glutDisplayFunc(myDisplay);
Therefore, before we actually draw any primitives, we need to set the values of some state
variables. The listing in Appendix A is for an OpenGL program that draws some points on the
display window, so before we draw the points we assign values for three state variables (at the
beginning of the myInit function):
In the first line we are defining the background colour to have an RGB value of (1,1,1), which
means white. When we clear the screen later on it will be cleared to white. The fourth argument to
glClearColor is the alpha value of the background colour. The alpha value indicates the opacity
of the colour (i.e. is it a semi-transparent colour?): a value of 1 means that it is completely opaque,
or the transparency is zero. The second line listed above defines the current drawing colour to be
(0,0,0), or black. Any subsequently drawn primitive will be drawn in this colour. The final line
sets the point size to four pixels. Any subsequent point primitives drawn will be drawn with this
size.
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, 640.0, 0.0, 480.0);
These lines define how the 3-D points will be projected onto the 2-D image plane. We will return
to this important subject in Chapter 5, but for the moment we will give a simplified version of what
really happens. We start off with our graphics primitives defined in world coordinates, then using
a 3-D to 2-D projection transformation we draw a picture of how the primitives would appear if
we took their picture using a virtual camera. The final primitives, drawn in the display window,
are in screen coordinates. Therefore we must specify to OpenGL how it should map points in
world coordinates to points in screen coordinates. The three lines given above define this mapping.
In effect, we are saying that any primitive whose x coordinate lies between 0 and 640, and whose
y coordinate lies between 0 and 480, will be seen by the virtual camera and therefore drawn in the
display window.
See Appendix A for a full listing of the simple OpenGL program we have worked through in this
section. In particular note the following routines in the display function:
glClear: This function is used to clear the screen before drawing. The background colour
set earlier will be used for clearing.
glBegin, glVertex2i, glEnd: This sequence of commands draws a number of point
primitives. Actually the glBegin … glEnd pair of commands is used to draw many different
types of primitive in OpenGL, with the symbolic constant argument to glBegin defining
how the vertices in between should be interpreted. In this case, GL_POINTS means that
each vertex should be considered to be a point primitive.
#include <GL/glut.h>
#include <stdlib.h>
void myInit(void) {
glClearColor(1.0, 1.0, 1.0, 0.0); // white background
glColor3f(0,0,0); // black foreground
glPointSize(4.0); // size of points to be drawn
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.
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.
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).
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 16, 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 17, we first compute a value for the gradient m:
yend
y 0 (13 10) 3
m 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
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
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 17 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
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_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.
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. (14):
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 16) we would end up with ‘holes’
in the line – see Figure 19. 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
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 20).
We can improve the efficiency of any circle-drawing algorithm by taking advantage of the
symmetry of circles. As illustrated in Figure 21, 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 21.
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 19), which meant that a time-
consuming gradient computation had to be performed for each point. In fact, the problem of holes
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 22. Recall that we only need to draw one octant of
the circle, as the other seven octants can be generated by symmetry. In Figure 22 we are drawing
the right-hand upper octant – the one with coordinate (y,x) in Figure 21, 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 22, 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 22 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)
KC Instr.H.Kedir Dept of cs Page 36
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)
r
x y
r
To understand these equations, refer to Figure 23. 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 24).
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.
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 25).
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 26.
(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.
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 29, 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 30(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 30(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 30(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).
(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 26.
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 31 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
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
32).
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 33. 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:
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 35. Notice that the
vertices of the polygon are specified in anti-clockwise order.
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 36 illustrates the use of the GL_TRIANGLES primitive.
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 37 illustrates the use of GL_TRIANGLE_STRIP.
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 39 for an example of GL_QUADS.
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 40 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 40 - 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 41(a)). This is a simple representation that allows fast rendering of the character.
An alternative representation to bitmaps is the stroke, or outline, representation (see Figure 41(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:
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:
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
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.
b. Nonzero winding number rule.
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.
In this handout we will consider the subject of attributes of graphics primitives. We can define an
attribute of a primitive as a parameter that affects the way the primitive is displayed. For example,
the drawing colour and line style are both attributes of the line primitive. We will examine what
attributes there are for different primitives, and look at some algorithms for implementing them.
Recall from Chapter 1 that OpenGL is a state system: it maintains a list of current state variables
that are used to modify the appearance of each primitive as it is displayed. Therefore these state
variables represent the attributes of the primitives. The values of state variables remain in effect
until new values are specified, and changes to state variables affects primitives drawn after the
change.
First we will look at colour attributes, which affect almost all primitives. The rest of this chapter
is structured according to the type of primitive that the attribute affects. We will look at attributes
of point primitives, line primitives, fill-area primitives and character primitives. For each we will
describe the underlying algorithms for implementing the change, and introduce how the attribute
can be modified in OpenGL. Finally we will consider the related topic of antialiasing.
2. Colour Attribute
In our first OpenGL program in Chapter 1 we defined values for two colour state variables: the
drawing colour and the background colour. We also saw that we can define colours in two different
ways:
RGB: the colour is defined as the combination of its red, green and blue components.
RGBA: the colour is defined as the combination of its red, green and blue components
together with an alpha value, which indicates the degree of opacity/transparency of the
colour.
In the simple program in Chapter 1, the background colour was defined with an alpha value,
whereas the drawing colour did not. If no alpha value is defined it is assumed to be equal to 1,
which is completely opaque.
It can often be useful to combine colours of overlapping objects in the scene or of an object with
the background colour. For example, this is necessary to achieve transparency effects and also for
antialiasing (see Section Error! Reference source not found.). This process is known as colour
blending, and the RGBA colour representation is necessary to achieve colour blending.
Recall that the frame buffer is used by raster graphics systems to store the image ready for display
on the monitor. Therefore, for colour displays, the frame buffer must have some way of storing
Using direct storage, the colour values of each pixel are stored directly in the frame buffer (see
Figure 42). Therefore, each pixel in the frame buffer must have 3 (RGB) or 4 (RGBA) values stored.
This means that the frame buffer can take up a lot of memory. For example, suppose we have a
screen resolution of 1024x768, and we use 8 bits for each of the red, green and blue components
of the colours stored. The total storage required will be 1024x768x24 bits = 2.4MB.
Colour look-up tables are also illustrated in Figure 42. They try to reduce the amount of storage
required by just storing an index for each pixel in the frame buffer. The actual colours are stored
in a separate look-up table, and the index looks up a colour in this table. Therefore, using colour
look-up tables we can only have a limited number of different colours in the scene (the number of
rows in the look-up table), but these colours can be chosen from a palette containing the complete
range of colours. For example, suppose we are using 8 bits for each colour (=24 bits in all), and
the look-up table has 256 rows. In this case the total range of colours we can choose from is 224,
but we can only use 256 of these in any one image.
Although colour look-up tables do save storage, they slow down the rasterisation process as an
extra look-up operation is required. They were commonly used in the early days of computer
graphics when memory was expensive, but these days memory is cheaper so most systems use
direct storage. OpenGL uses direct storage by default, but the programmer can choose to use a
colour look-up table if they wish.
We have already seen how to modify the drawing and background colours in the simple OpenGL
program in Chapter 1. To recap, we change the current drawing colour using the glColor* function
and the current background colour using the glClearColor function. The background colour is
always set using an RGBA colour representation (although, as we will see below, the alpha value
is sometimes ignored). The drawing colour can be set using either RGB or RGBA representations.
The following are all valid examples of setting the drawing and background colours:
Before we do any drawing in OpenGL, we must define a frame buffer. If we want to do colour
drawing then we must specify that we want a colour frame buffer. Again, we saw how to do this
in Chapter 1, but to recap the following line defines a single direct storage colour frame buffer
with red, green and blue components:
glutInitDisplayMode(GLUT_SINGLE,GLUT_RGB)
In order to store alpha values in the frame buffer we must change the second argument to
GLUT_RGBA. To specify that we want to use a colour look-up table we set the second argument
equal to GLUT_INDEX.
3. Point Attributes
Points are the simplest type of primitive so the only attributes we can modify are the colour and
size of the point. The simple program given in Chapter 1 showed how to modify each of these state
variables in OpenGL. To change the size of points in pixels we use:
glPointSize(size)
Points are drawn in the current drawing colour. Section 2.2 above showed how to change the
drawing colour. All points in OpenGL are drawn as squares with a side length equal to the point
size. The default point size is 1 pixel.
4. Line Attributes
The attributes of line primitives that we can modify include the following:
Line colour
Line width
Line style (solid/dashed etc)
We have already seen how to change the drawing colour. In the following sections we examine
the line width and line style attributes.
The simplest and most common technique for increasing the width of a line is to plot a line of
width 1 pixel, and then add extra pixels in either the horizontal or vertical directions. This concept
is illustrated in Figure 43. We can see that for some lines (e.g. Figure 43(a)) we should plot extra
pixels vertically, and for others (e.g. Figure 43(b)) we should plot them horizontally. Which of
these two approaches we use depends on the gradient m of the line. We identify the following
cases:
If |m| ≤ 1 plot extra pixels vertically.
If |m| > 1, plot extra pixels horizontally.
(a) (b)
Figure 43 - Increasing Line Width by Plotting Extra Pixels in the Vertical (a) and
Horizontal (b) Directions
Although this approach is simple and effective, there are two slight problems:
The actual thickness of a plotted line depends on its slope. This is illustrated in Figure 44.
Although this is a small weakness of the technique most graphics packages do not attempt
to address this problem. You can notice this effect in, for example, the Paint accessory in
Microsoft Windows.
The ends of lines are either vertical or horizontal. This is illustrated in Figure 45.
Depending on whether we are plotting extra pixels in the horizontal or vertical directions,
the line ends will be horizontal or vertical.
The answer to the second problem (horizontal/vertical line ends) is to use line caps. A line cap is
a shape that is applied to the end of the line only. Three types of line cap are common in computer
graphics:
Butt cap
Round cap
Projecting square cap
These are illustrated in Figure 46. The butt cap is formed by drawing a line through each end-point
at an orientation of 90o to the direction of the line. The round cap is formed by drawing a semi-
circle at each end-point with radius equal to half the line width. The projecting square cap is similar
to the butt cap but the position of the line is extended by a distance of half the line width.
These line caps effectively solve the problem of vertical/horizontal line endings, but in the process
they introduce a new problem. If we are drawing polylines, or a connected series of line segments,
with these line caps, we can get problems at line joins. For example, Figure 47 shows what happens
at the line join of two line segments drawn with butt caps. There is a small triangular area at the
join that does not get filled.
The style of a line refers to whether it is plotted as a solid line, dotted, or dashed. The normal
approach to changing line style is to define a pixel mask. A pixel mask specifies a sequence of bit
values that determine whether pixels in the plotted line should be on or off. For example, the pixel
mask 11111000 means a dash length of 5 pixels followed by a spacing of 3 pixels. In other words,
if the bit in the pixel mask is a 1, we plot a pixel, and if it is a zero, we leave a space.
OpenGL allows us to change both the line width and the line style. To change the line width we
use the glLineWidth routine. This takes a single floating point argument (which is rounded to the
nearest integer) indicating the thickness of the line in pixels. For example,
glLineWidth(4.3)
will set the line width state variable to 4 pixels. This value will be used for all subsequent line
primitives until the next call to glLineWidth. The ‘true’ line thickness will depend on the
orientation of the line.
The line style is specified using a pixel mask. First we must enable the line stipple feature of
OpenGL using the following line:
glEnable(GL_LINE_STIPPLE)
Next, we use the glLineStipple function to define the line style. glLineStipple takes two arguments:
a repeat factor, which specifies how many times each bit in the pixel mask should be repeated,
and a pattern, which is a 16-bit pixel mask. For example, the code shown below draws the dashed
line shown to the right. The pixel mask is the hexadecimal number 00FF, which is 8 zeros followed
by 8 ones. The repeat factor is 1 so each one or zero in the pixel mask corresponds to a single pixel
in the line.
glEnable(GL_LINE_STIPPLE);
glLineWidth(3.0);
glLineStipple(1, 0x00FF);
glBegin(GL_LINE_STRIP);
glVertex2i(100,100);
glVertex2i(150,100);
glVertex2i(150,200);
glVertex2i(250,250);
glEnd();
glDisable(GL_LINE_STIPPLE);
The most important attribute for fill-area polygons is whether they are filled or not. We can either
draw a filled polygon or drawn the outline only. Drawing the outline only is known as wireframe
rendering. If the polygon is to be filled we can specify a fill style.
In this section we first consider some algorithms for filling polygons and other fill-area primitives.
Then we describe how to modify fill-area attributes in OpenGL.
Broadly speaking, we can identify two different approaches to filling enclosed boundaries:
Scan-line approach: We automatically determine which pixels are inside or outside the
boundary for each scan line. These approaches are normally used by general-purpose
graphics packages.
Seed-based approach: Start from an interactively defined seed point and paint outwards
until we reach a boundary. These approaches are normally used by special-purpose
graphics packages.
The scan-line fill algorithm automatically fills any non-degenerate polygon by considering each
scan-line (i.e. row of the frame buffer) in turn. We move across the scan-line, from left-to-right,
until we reach a boundary. Then we start plotting pixels in the fill colour and continue to move
across the scan-line. When we reach another boundary we stop filling. At the next boundary we
start filling again, and so on. We can think of this algorithm as being similar to the odd-even rule
inside-outside test: the starting point (i.e. the far left of the scan-line) is assumed to be outside the
polygon, so points between the first and second boundary crossings are assumed to be inside. This
process is illustrated in Figure 49.
There are two possible answers to this problem. First, we can perform a preprocessing stage to
detect which vertices cross the scan-line and which don’t. This would work OK, but it also takes
time. Therefore the more common approach is to insist that all polygons are convex. As we can
see from Figure 51, in a convex polygon there is always a maximum of two boundary crossings in
every scan-line. Therefore if there are two crossing we simply fill between the two crossing points.
If there is one we know that it is a vertex crossing so we fill a single pixel. This greatly improves
the efficiency of the scan-line fill algorithm, and is the reason why most graphics packages insist
that all fill-area primitives are convex polygons.
Figure 50 - Problems in the Scan-Line Fill Algorithm: Scan-Line 'x' is Filled Correctly but
Scan-Line 'y' is not
Seed-based fill algorithms have the advantage that they can fill arbitrarily complex shapes. They
are less efficient than scan-line algorithms, and require user interaction in the form of specifying
a seed-point (i.e. a starting point for the fill, known to be inside the boundary). Therefore they are
typically used by interactive packages such as paint programs.
In this section we will examine two similar techniques: boundary-fill and flood-fill. Both are
recursive in nature and work by starting from the seed point and painting outwards until some
boundary condition is met. The difference between the two algorithms is in the nature of the
boundary condition.
To implement the boundary fill algorithm we first define three parameters: the seed point, a fill
colour and a boundary colour. The pseudocode for the algorithm is:
Start with seed point
If the current pixel is not in the boundary colour and not in the fill colour:
o Fill current pixel using fill colour
o Recursively call boundary-fill for all neighbouring pixels
Notice that the termination condition for the algorithm is when we reach pixels in the boundary
colour. Therefore the boundary must be in a single (known) colour. We check to see if the current
pixel is in the fill colour to ensure we don’t fill the same pixel many times.
One question that we must answer before implementing boundary-fill is what exactly we mean by
a neighbouring point. Figure 52 shows that there are two possible interpretations of a neighbour:
4-connected and 8-connected neighbours. Both interpretations can be useful, although using 8-
connected neighbours can lead to the algorithm ‘escaping’ from thin diagonal boundaries, so 4-
connected neighbours are more commonly used.
Flood-fill is very similar to boundary-fill, but instead of filling until it reaches a particular boundary
colour, it continues to fill while the pixels are in specific interior colour. Therefore, first we must
define a seed point, a fill colour and an interior colour. Pseudocode for flood-fill is given below:
Start with seed point
If current pixel in interior colour:
o Fill pixel using fill colour
o Recursively call flood-fill for all neighbouring pixels
OpenGL provides a number of features to modify the appearance of fill-area polygons. First, we
can choose to fill the polygon or just display an outline (i.e. wireframe rendering). We do this by
changing the display mode using the glPolygonMode routine. The basic form of this function is:
glPolygonMode(face, displayMode)
Here, the face argument can take any of the following values:
GL_FRONT: apply changes to front-faces of polygons only.
GL_BACK: apply changes to back-faces of polygons only.
GL_FRONT_AND_BACK: apply changes to front and back faces of polygons.
For example, the following code draws a polygon with four vertices using wireframe rendering for
the front face.
In addition to changing the polygon drawing mode, we can also specify a fill pattern for polygons.
We do this using the polygon stipple feature of OpenGL. The steps can be summarised as:
Define a fill pattern
Enable the polygon stipple feature of OpenGL
Draw polygons
For example, the following code draws an eight-sided polygon with a fill pattern. The fill pattern
accepted by the glPolygonStipple function must be an array of 128 8-bit bitmaps. These bitmaps
represent a 32x32 pixel mask to be used for filling. The image produced by the code below is
shown in Figure 55.
In this handout we will introduce some important theoretical foundation for the next chapter on
the viewing pipeline. We will review the mathematics of matrix transformations, and see how
matrices can be used to perform different types of transformation: translations, rotations and
scalings. We will introduce the important topic of homogeneous coordinates – a variation on the
standard Cartesian coordinate system that is widely employed in computer graphics. Finally we
will consider how transformations can be defined in OpenGL.
First of all let us review some basics of matrices. 2x2 matrices can be multiplied according to the
following equation.
a b e f ae bg af bh
…………………………………… (1)
c d g h ce dg cf dh
For example,
3 1 1 2 3 1 1 2 3 2 1 0 1 6
2 1 2 0 2 1 1 2 2 2 1 0 4 4
Matrices of other sizes can be multiplied in a similar way, provided the number of columns of the
first matrix is equal to the number of rows of the second.
Matrix multiplication is not commutative. In other words, for two matrices A and B, AB≠BA. We
can see this from the following example.
1 2 3 1 5 1
2 0 2 1 6 2
3 1 1 2 1 6
2 1 2 0 4 4
However, matrix multiplication is associative. This means that if we have three matrices A, B and
C, then (AB)C = A(BC). We can see this from the following example.
In the following sections we will consider how to perform certain common types of coordinate
transformations using matrices. We will start off by looking only at 2-D points, i.e. points that
have an x and a y coordinate. Later in this chapter we will extend our discussion to 3-D points.
The translation transformation shifts all points by the same amount. Therefore, in 2-D, we must
define two translation parameters: the x-translation tx and the y-translation ty. A sample translation
is illustrated in Figure 56.
px
P ………………………………………………………………….. (2)
py
px
P ………………………………………………………………….. (3)
py
tx
T …………………………………………………………………... (4)
ty
px px t x
………………………………………………………….. (5)
p p t
y y y
Therefore, from Eq. (5) we can see that the relationship between points before and after the
translation is:
The rotation transformation rotates all points about a centre of rotation. Normally this centre of
rotation is assumed to be at the origin (0,0), although as we will see later on it is possible to rotate
about any point. The rotation transformation has a single parameter: the angle of rotation, θ. A
sample rotation in 2-D about the origin is illustrated in Figure 57.
cos sin
R …………………………………………………………. (8)
sin cos
px cos sin px
…………………………………………. (9)
p
y sin cos p y
Therefore, from Eq. (9) we can see that the relationship between points before and after the rotation
is:
The scaling transformation multiplies each coordinate of each point by a scale factor. The scale
factor can be different for each coordinate (e.g. for the x and y coordinates). If all scale factors are
equal we call it uniform scaling, whereas if they are different we call it differential scaling. A
sample scaling is shown in Figure 58.
Sx 0
S ………………………………………………………………….
(12)
0 Sy
px S x 0 px
…………………………………………………. (13)
p 0 S p
y y y
Therefore, from Eq. (13) we can see that the relationship between points before and after the
scaling is:
3. Homogeneous Coordinates
In the next chapter we will look at the viewing pipeline for computer graphics: a sequence of
transformations that every primitive undergoes before being displayed. As we must perform a
sequence of transformations in this pipeline, it is essential that we have an efficient way to execute
these transformations.
One answer is to compose the series of transformations into a single matrix, and then apply the
composed matrix to every primitive. This would be efficient because we perform the composition
once only (the transformation will be the same for all primitives), leaving only a single matrix
multiplication for each primitive. Since matrix multiplications are often executed in dedicated
hardware on the video card this will be very fast.
Example 1
We want to transform a large number of points by the same sequence of three matrix
transformations: a rotation R1, followed by a scaling S1 and finally another rotation R2. In this case,
the overall transformation is P R2 S1R1P . Therefore we can implement this by composing R2, S1
and R1 into a single composite matrix C, and then multiplying each point by C.
Example 2
We want to transform a large number of points by the same sequence of four matrix
transformations: a translation T1, a rotation R1, another translation T2 and finally a scaling S1. In
this case, the overall transformation can be expressed as
P S1 R1 P T1 T2 S1R1P S1R1T1 S1T2 . Clearly this is significantly more complex than the
previous example. Even if we combined S1R1, S1R1T1 and S1T2 into composite matrices we would
still have to apply two extra matrix additions for every point we wanted to transform. Therefore
the operation of the graphics pipeline would be much slower in this second example compared to
the first. The reason is that this second sequence of transformations included translations, whereas
the first sequence consisted of only rotations and scalings. Using the definitions we have seen so
far rotations and scalings are performed using matrix multiplications, whereas translations are
performed using matrix additions. We could improve the efficiency of the graphics pipeline if we
could find a way to express translations as matrix multiplications.
Homogeneous coordinates allow us to do just this. With homogeneous coordinates we add an extra
coordinate, the homogenous parameter, to each point in Cartesian coordinates. So 2-D points are
stored as three values: the x-coordinate, the y-coordinate and the homogeneous parameter. The
relationship between homogeneous points and their corresponding Cartesian points is:
x x/h
Homogeneous point = y , Cartesian point = y / h
h 1
Normally the homogenous parameter is given the value 1, in which case homogenous coordinates
are the same as Cartesian coordinates but with an extra value which is always 1. In the following
sections we will see how adding this extra homogeneous parameter helps us to express translation
transformations using matrix multiplications.
Now we can express a translation transformation using a single matrix multiplication, as shown
below.
px
P py …………………………………………………………………. (16)
1
px
P py …………………………………………………………………. (17)
1
1 0 tx
T 0 1 ty …………………………………………………. (18)
0 1
0
px 1 0 t x px
py 0 1 t y p y ………………………………………………… (19)
1 0 0 1 1
Therefore px px t x , py py t y , exactly the same as before, but we used a matrix
multiplication instead of an addition.
Rotations can also be expressed using homogenous coordinates. The following equations are
similar to the form of the 2-D rotation given in Eqs. (8)-(11), with the exception that the rotation
matrix R has an extra row and and extra column.
cos sin 0
R sin cos 0 ………………………………………………….. (20)
0 1
0
px cos sin 0 px
py sin cos 0 p y ………………………………………….. (21)
1 0 1 1
0
Therefore px px cos py sin and py py cos py sin , which is the same outcome as
before.
Finally, we can also express scalings using homogeneous coordinates, as shown by the following
equations.
Sx 0 0
S 0 Sy 0 ………………………………………………………….. (22)
0 1
0
px S x 0 0 px
py 0 Sy 0 p y ………………………………………………….. (23)
1 0 1 1
0
4. Matrix Composition
An example of this sequence of transformations is shown in Figure 59. Here we perform a rotation
about the pivot point (2,2) by translating by (-2,-2) to the origin, rotating about the origin and then
translating by (2,2) back to the pivot point. Let us denote our transformations as follows:
T1 is a matrix translation by (-2,-2)
R is a matrix rotation by θo about the origin
T2 is a matrix translation by (2,2)
Therefore, using homogenous coordinates we can compose all three matrices into one composite
transformation, C:
The composite matrix C can now be computed from the three constituent matrices T2, R and T1,
and represents a rotation about the pivot point (2,2) by θo. Note from Eq. (24) that T1 is applied
first, followed by R and then T2. For instance, if we were to apply the three transformations to a
point P the result would be
Therefore because T1 is right next to the point P it gets applied first, followed by the next
transformation to the left, R, and so on.
The concept of homogenous coordinates is easily extended into 3-D: we just introduce a fourth
coordinate in addition to the x, y and z-coordinates. In this section we review the forms of 3-D
translation, rotation and scaling matrices using homogeneous coordinates.
The 3-D homogeneous coordinates translation matrix is similar in form to the 2-D matrix, and is
given by:
1 0 0 t
0 1 0 ty
T
0 0 1 tz ………………………………………….. (25)
0 1
0 0
px 1 0 0 t px
py 0 1 0 t y p y
p 0 0 1 t z pz
………………………………………………….. (26)
z
1 0 0 1 1
0
Similarly, 3-D scalings are defined by three scaling parameters, Sx, Sy and Sz. The matrix is:
Sx 0 0 0
0 Sy 0 0
S
0 0 Sz 0
0 1
0 0
px S x 0 0 0 px
py 0 Sy 0 0 p y
p 0 0 Sz 0 pz
z
1 0 1 1
0 0
For rotations in 3-D we have three possible axes of rotation: the x, y and z axes. (Actually we can
rotate about any axis, but the matrices are simpler for x, y and z axes.) Therefore the form of the
rotation matrix depends on which type of rotation we want to perform.
1 0 0 0
0 cos sin 0
Rx
0 sin cos 0
0 1
0 0
cos 0 sin 0
0 1 0 0
Ry
sin 0 cos 0
0 1
0 0
cos sin 0 0
sin cos 0 0
Rz
0 0 1 0
0 1
0 0
When referring to 3-D coordinate systems, we can distinguish between right-handed and left-
handed coordinate systems. This concept is illustrated in Figure 60. For a right-handed coordinate
system, if we extend the thumb and first two fingers of our right-hand so that they are perpendicular
to each other, then the first finger represents the direction of the x-axis, the second finger the y-
axis and then thumb points in the direction of the z-axis. Contrast this to a left-handed coordinate
system in which we do the same thing with our left hand. In this case, if we align the x and y axes
of the right-handed and left-handed coordinate systems, the z-axes will point in opposite directions.
The most common type of coordinate system used in computer graphics is the right-handed
coordinate system, but when using a general purpose graphics package it is important to know
which type of coordinate system it uses.
Before we look at how to define matrix transformations in OpenGL we must introduce the concepts
of premultiplying and postmultiplying. Whenever a matrix is multiplied by another existing matrix
we can either premultiply or postmultiply. For example, if we premultiply matrix A by matrix B,
the result will be BA. If we postmultiply matrix A by matrix B the result will be AB. Often, when
using a general purpose graphics package we need to specify a sequence of transformations, so we
need to know whether the package will compose these transformations by premultiplying or
postmultiplying. This is very important because which of these two techniques the package uses
determines the order in which we should specify our transformations. For example, suppose we
specify a sequence of matrix transformations A, B and C (in this order). Using premultiplying, the
composite transformation will be CBA, whereas using postmultiplying it will be ABC. We have
already seen in Section 2 that matrix multiplication is not commutative, so these two results will
be different. We can see from this example that when postmultiplying we must define our sequence
of transformations in the reverse order to that in which we want them to be applied. The result of
postmultiplying the matrices A, B and C is ABC, so C is applied first, followed by B and then A.
Now let us look at OpenGL functions for defining transformations. In total, there are six functions
that affect the current matrix:
glTranslate*(tx,ty,tz): Postmultiply the current matrix by a translation matrix formed from
the translation parameters tx, ty and tz.
glRotate*(θ,vx,vy,vz): Postmultiply the current matrix by a rotation matrix that rotates by
θo about the axis defined by the direction of the vector (vx,vy,vz).
glScale*(Sx,Sy,Sz): Postmultiply the current matrix by a scaling matrix formed from the
scale factors Sx, Sy and Sz.
glLoadMatrix*(elements16): Replaces the current matrix with the 16-element array
element16. The array should be defined in column-major order (i.e. the first four elements
represent the first column; the next four represent the second column, etc.).
glMultMatrix*(elements16): Postmultiplies the current matrix with the 16-element array
element16. The array should be defined in column-major.
glLoadIdentity(elements16): Replaces the current matrix with a 4x4 identity matrix.
Each current matrix in OpenGL has an associated matrix stack. This is a standard FIFO stack that
can be used to ‘remember’ different transformations. In fact, the current matrix is actually just the
top matrix on the matrix stack. We will see in the next chapter why matrix stacks can be useful,
but for the moment let us introduce the two functions for manipulating the stack:
glPushMatrix: Copy the current matrix to the next position down in the stack, push all other
matrices down one position. The current matrix (i.e. the top matrix on the stack) is left
unchanged.
To finish this chapter, let us look at an example of using these OpenGL routines to define a
composite transformation. Consider the following code:
glLoadIdentity();
glTranslated(2.0, 2.0, 0.0);
glRotated(90.0, 0.0, 0.0, 1.0);
glTranslated(-2.0, -2.0, 0.0);
This is actually the same example as we saw above in Figure 59: a rotation about the pivot point
(2,2). Note from this example that we define the transformations in reverse order (because OpenGL
always postmultiplies). This example uses 2-D graphics so the rotation is performed about the z-
axis.
Summary
Most graphics packages use a right-handed coordinate system: we can visualise the axes
of a right-handed coordinate system by extending the thumb and first two fingers of the
right hand so that they are perpendicular: the first finger is the x-axis, the second finger is
the y-axis and then thumb is the z-axis.
If we premultiply matrix A by matrix B the result is BA. If we postmultiply A by B the result
is AB.