OpenGL was chosen as the graphics API for the development of the simulation as the OpenGL specification is an open standard that supports lighting and shading, texture mapping, blending, transparency and animation. OpenGL is available across many different operating systems and hardware platforms including Microsoft Windows, MacOS X and the X Window System on UNIX and Linux  and is "the most widely used 3D graphics API in the industry" .
This section reviews some of the tools and techniques provided by OpenGL for the creation and manipulation of two- and three-dimensional images that were used in the simulation of visual impairments.
Primitives in OpenGL include points, triangles, quads, line strips, line loops, triangle strips, triangle fans, quad strips and polygons:
Vertices are specified by either two- or three-dimensional coordinates using glVertex() and are listed within a glBegin()…glEnd() block .
OpenGL does not have functions for drawing curves or circles. Instead an approximation of a curve can be drawn by using straight lines . An approximation of a circle can be drawn by using a polygon with n sides. Setting n = 60 gives a reasonable approximation. Increasing the value of n produces a smoother circle. Each vertex of the polygon (x, y) is calculated using the trigonometric equations:x = radius * sin (angle); y = radius * cos (angle);
The value of the angle is incremented by 360/n degrees for each vertex, where n is the number of sides.
OpenGL supports both perspective and orthographic (parallel) projections. In an orthographic projection, objects that have the same dimensions are the same size in the image, regardless of their distance from the viewer. A perspective projection adds the effect of distance to the image. Objects close to the viewer appear larger than objects that have the same dimensions, but are further away .
Projections are set up by specifying the dimensions of the viewing volume using glFrustrum() for perspective projections and glOrtho() for parallel projections. The viewport defines the area within the window in actual screen coordinates that OpenGL can use to draw in. It is specified using glViewPort() each time the window is resized .
OpenGL makes use of several pixel buffers. The colour buffer, also known as the frame buffer contains the pixels that are to be displayed. If a single colour buffer is used, objects are displayed as soon as they are rendered. Double buffering uses two colour buffers. Each frame is first rendered into the 'back' buffer, and then swapped into the 'front' buffer for display. This technique is used to provide smooth animations. It can also be used to allow complex scenes to be rendered completely before being displayed .
The accumulation buffer allows the contents of the colour buffer to be repeatedly blended together and then copied back to the colour buffer for display. The depth buffer is used during the rendering of three-dimensional scenes for depth testing and hidden-surface removal. The stencil buffer contains the stencil pattern used to perform the stencil test (Section 7.1.5).
To improve performance, OpenGL allows rendering to be restricted to a portion of the screen that requires updating. Rendering is restricted to a rectangular region within a window by using a scissor box, or an irregularly shaped region using a stencil pattern.
The scissor box is created using glScissor(). The scissor test is enabled and disabled using glEnable(GL_SCISSOR) and glDisable(GL_SCISSOR).
The stencil pattern is created in the stencil buffer using normal OpenGL drawing commands and is used to decide which pixels are drawn. Only pixels which pass the stencil test are drawn to the colour buffer. The stencil test is specified using the functions glStencilOp() and glStencilFunc(). It is enabled and disabled using glEnable(GL_STENCIL_TEST) and glDisable(GL_STENCIL_TEST) .
A display list is a precompiled list of OpenGL commands. Each display list is assigned a name which can be any unsigned integer. The function glGenLists() can be used to generate unique display list names. OpenGL drawing instructions are listed within a glNewList()…glEndList() block. The display list is then executed using glCallList(). The function glDeleteLists() frees display list names and releases any memory allocated .
Each pixel has a separate red, green and blue component. Typically one byte is used for each component, resulting in 224 colours. An optional fourth component, the alpha component, can be used to specify a transparency value. When drawing objects, each vertex of a primitive can be assigned a colour by specifying the RGBA colour components with the function glColour4f(red, green, blue, alpha) .
An indexed colour system can be used to limit the number of colours in use and therefore reduce the memory requirements of the system. A colour-lookup table is constructed by selecting colours from a colour palette. Typically one byte is used as an index resulting in a colour table with 256 entries .
Primitives in OpenGL can be shaded using either a flat or a smooth shading model. With flat shading enabled, each point within a primitive is assigned the same shade. With smooth shading enabled, colours between vertices of primitives are interpolated to produce the effect of a smooth transition from one colour to another. The shading model is selected by calling glShadeModel() with the parameter GL_FLAT or GL_SMOOTH .
Dithering is a technique that can be used to simulate displaying a wider range of colours than are actually supported by the display hardware. A mixture of colours is used to create the illusion of a new colour. OpenGL allows dithering to be enabled or disabled using glEnable(GL_DITHER) and glDisable(GL_DITHER) .
Transparency can be achieved in OpenGL by using blending during the rendering phase. In addition to the red, green and blue colour components, each object has an optional alpha component that defines a transparency value for the object. When an object is rendered with blending enabled, the object's colour (the source colour) is combined with the colour already in the colour buffer (the destination colour). The final colour is determined by the equation:Final Colour = (Source Colour * Source Blending Factor) + (Destination Colour * Destination Blending Factor)
OpenGL blending is enabled with glEnable(GL_BLEND) and disabled with glDisable(GL_BLEND). The source and destination blending factors are specified by glBlendFunc(sfactor, dfactor) .
"Fog is an atmospheric effect that adds haziness to objects in a scene" . In OpenGL, a fog colour is blended with the specified scene. The amount of blending is determined by the distance of each object from the centre of projection. A fog starting point specifies how far away an object must be before fog is applied. A fog end point specifies the distance that objects become completely obscured by the fog. The transition of the fog between the start and end points can be either linear or exponential. Fog is enabled with glEnable(GL_FOG) and disabled with glDisable(GL_FOG). The function glFog() is used to specify the fog colour, the start and end points, the fog transition between these points, and the density of the fog .
After the rasterisation stage of the graphics pipeline (Section 3.8.3), the modelled scene will be represented as a two-dimensional image in the frame buffer. OpenGL has a collection of operations for reading, writing and manipulating pixels in the frame buffer.
The contents of the colour buffer can be saved as a pixmap (an array of pixels) using glReadPixels() and written to using glDrawPixels(). Pixels can also be copied from one part of the buffer to another using glCopyPixels(). A parameter describes the format of the pixmap data. OpenGL pixel formats include GL_RGB, GL_RGBA, GL_BGR, GL_BGRA, GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA and GL_LUMINANCE. The format parameter can also be used to specify that pixels be read from, or written directly to the depth and stencil buffers .
The image can be stretched or shrunk by specifying x and y zoom factors to the function glPixelZoom(). A negative zoom factor has the effect of inverting the image .
Simple mathematical operations can be applied to the image pixels as the image is transferred to or from the colour buffer. A pixel transfer mode is specified using glPixelTransfer(). Individual colour channels can be adjusted by multiplying by a scaling factor and then adding a bias value:New Value = (Old Value * Scale Value) + Bias Value
By default, no adjustment is made when pixels are transferred to and from the colour buffer. The scale value is 1.0 and the bias value is 0.0 .
A colour map is a lookup table used to convert colour values. It is applied as the image is transferred to or from the colour buffer. The pixel map is set up using the function glPixelMap() and is enabled by calling glPixelTransfer(GL_MAP_COLOR, GL_TRUE) .
Effects such as sharpening (edge detection), smoothing (blurring), dilation and erosion can be applied to an image using a convolution filter. A two-dimensional kernel is applied to every pixel in the image .
The kernel is a pattern of weights that describe how each pixel in the image is combined with its neighbouring pixels. The kernel is stored as a two-dimensional array of floating point numbers and is applied to the image by calling glConvolutionFilter2D() before the pixels are written to the colour buffer using glReadPixels(). Convolution filters are enabled and disabled using glEnable(GL_CONVOLUTION_2D) and glDisable(GL_CONVOLUTION_2D) .
The glConvolutionFilter2D() operation is part of the OpenGL imaging subset which is an extension to the original OpenGL specification and may not be supported by all implementations. It is possible to determine at runtime whether the imaging subset is supported using gltIsExtSupported("GL_ARB_imaging"). An alternative way of implementing the convolution operation is to build the output image in the accumulation buffer by repeatedly translating the image in the colour buffer and blending it with the contents of the accumulation buffer .
The convolution filter can also be applied by using a fragment shader (Section 7.6) to post process the image. This requires copying the contents of the frame buffer to a texture and applying the kernel to samples in a neighbourhood using texture lookups .
Texture mapping is the process of applying a pattern or texture to geographical objects as they are rendered and is often used to make a scene more realistic. OpenGL supports the mapping of one-, two- and three-dimensional textures. A texture is regarded as an array of texture elements or texels, rather than pixels. Each texel is addressed by a texture coordinate. A texture is mapped to a geographic primitive by specifying a texture coordinate for each vertex.
Conventionally, OpenGL rendering uses a fixed functionality pipeline (Section 3.8.3) which performs a sequence of distinct operations on the primitives from the modelling stage. The geometric processing stages of the pipeline operate on a per-vertex basis and include transformation, lighting and clipping. Each primitive is then rasterised to produce fragments. Per-fragment operations including texturing, antialiasing, blending and dithering are applied before each fragment is written to the framebuffer .
The per-vertex and per-fragment stages of the pipeline can be replaced by programmable vertex shaders and fragment shaders. The OpenGL extensions, GL_ARB_vertex_program and GL_ARB_fragement_program are low-level shaders which use an assembly-like language. The high-level shading extensions GL_ARB_vertex_shader and GL_ARB_fragment_shader are based on the OpenGL Shading Language (GLSL) .
The platform independence of OpenGL means that there are no functions for window management or user interaction. GLUT, the GL Utility Toolkit is an additional library that provides a generic interface to the window system and is available on most platforms. An event-driven model is used for handling input. Callback functions that respond to input events are registered with the GLUT event loop .
A window is created using the function glutCreateWindow(). The function glutInitDisplayMode() is used to specify the properties of the window such as whether single or double buffering is used for the colour buffer, and which other buffers are to be created. The GLUT event loop is started using glutMainLoop().
The display function registered with glutDisplayFunc() is called whenever the contents of the window require redrawing. An explicit request can be made to the GLUT event loop to refresh the contents of the window by calling glutPostRedisplay(). If double-buffering is used, rendering takes place in the back colour buffer. To update the window contents, the front and back buffers must be swapped using glutSwapBuffers().
The reshape function registered with glutReshapeFunc() is called whenever the window is resized and is passed the new dimensions of the window .
GLUT recognises three types of mouse events. A mouse click event is generated each time one of the mouse buttons is depressed or released. The button state (up or down) and the screen coordinates of the cursor are passed to the callback function registered using glutMouseFunc(). A motion event is generated when the mouse, or other pointing device is moved while one of the buttons is depressed. If the mouse is moved with no button depressed, a passive motion event is generated. The screen coordinates of the cursor are passed to the appropriate callback function. The callback functions are registered using glutMotionFunc() and glutPassiveMotionFunc().
GLUT allows callback functions to be registered for two types of keyboard events. A normal keyboard event is generated when an ASCII key is pressed. There is no event generated for the release of a key. The ASCII character code for the key and the screen coordinates of the cursor are passed to the callback function registered using glutKeyboardFunc(). Non-ASCII keystokes such as the function keys F1 - F12 generate a special key event. An enumerated value is passed to the callback function registered using glutSpecialFunc().
GLUT provides hierarchical popup menus that can be linked to a mouse button. The functions used to construct the menus are glutCreateMenu(), glutAddMenuEntry(), and glutAddSubMenu(). The menu is assigned to a mouse button using glutAttachMenu(). Callback functions are used to respond to selections from the menu .
OpenGL does not have a text primitive. GLUT has the functions glutStrokeCharacter() and glutBitmapCharacter(). Stroke characters are vector based and are sized and positioned using glScale() and glTranslate(). Bitmap characters are raster based. Their size is determined by the selected font and their position is set using glRasterPos().
OpenGL is a three-dimensional graphics API. It supports the modelling of geometric primitives, lighting and shading, texture mapping, fog, blending, transparency and animation. It also supports pixel operations on two-dimensional images such as pixel transfer and pixel zoom, and convolution filters for image sharpening and softening.
The GL Utility Toolkit (GLUT) provides an interface between OpenGL and the operating system. It has functions for managing windows and handling event-driven input from the user.
© Stephen Ratcliffe. 2005
Department of Computer Science
University of York