Spriting with HOpenGL

Table of Contents

The Groundwork : What This Tutorial Assumes
HOpenGL : An Introduction
Primitives and Textures in HOpenGL : A Primer
Drawing Sprites in HOpenGL : Sprites.hs
Setting Up : The spriteInit Function
Reading Images Into Memory With HOpenGL : pngtorgb and ReadImage.hs
Texture Creation and Loading : createTexture and createTextures
Actually Drawing Sprites : displaySprite and displaySpriteWithFrame
The Sprites Module in Action : SpritingDemo.hs
The End : Compiling and Running the Programs
The Sources : PngToRgb.tar.gz and SpritingDemo.tar.gz


You can head back to our main page here.


The Groundwork : What This Tutorial Assumes

This tutorial is written for people who have decent or great knowledge of Haskell and wish to learn HOpenGL. This tutorial assumes knowledge of Haskell syntax, partial evaluation of functions, monads, and the like. If you are not fairly comfortable with Haskell or do not know how to look up Haskell functions, you will benefit more from this tutorial by reading others beforehand. If you already know C++ or languages like it, this tutorial may be just the thing you need.

This tutorial focuses on drawing flat images to the screen using HOpenGL, although the code and ideas in this tutorial can also be applied to 3D imaging, as well. This is half a tutorial and half a manual for the spriting module (Sprites.hs) written by David Morra and Eric Etheridge. The source code for the module is freely available to anyone who wishes to use / alter it.

For those who are ready to go, let's jump in:

HOpenGL : An Introduction

HOpenGL is the Haskell binding for the multiplatform graphics API OpenGL. The binding was created by Sven Panne and is currently a work in progress. Other than this tutorial, there are a few resources a budding HOpenGL programmer may use to learn more about the binding:

Sven Panne's HOpenGL Homepage.

A practical HOpenGL primer. Note that this tutorial is slightly out of date and does not cover subjects like texturing and the like. The HOpenGL API has evolved since this tutorial was written, so the reader should be aware that the code found within may not compile with the most recent version of HOpenGL.

Another practical primer. This tutorial is very out of date and I only reccomend that you use it as a tutorial of concept. Much of the syntax found in this tutorial is out of date, but it could still be helpful for someone looking to learn about the architecture of an HOpenGL program.

FunGEn, a game engine written in Haskell with HOpenGL as the underlying API. I have personally not used this engine before, so I can't attest to whether it is a good resource. The reader should use it at their own risk.

If you know of another HOpenGL resource that you would like linked on this tutorial, please feel free to send me mail and I'll see what I can do about adding it.

Primitives and Textures in HOpenGL : A Primer

OpenGL does its primary work by rendering primitive polygons with textures on them to the screen. Primitive are things like Triangles, Quadrilatterals (called Quads), Polygons, Triangle Strips, Quad Strips, Lines and Points. Since primitives are drawn so often, OpenGL has a built-in method of rendering them to the screen. In the most recent version of HOpenGL, this function is called renderPrimitive. Its Haskell type is:

renderPrimitive :: PrimitiveMode -> IO a -> IO a

renderPrimitive will take the kind of primitive you wish to render (Quads, Triangles, etc) and a monadic function which is essentially a set of HOpenGL commands. The commands do things like set the locations of the primitive's verticies, set the color of the primitive at a vertex, and maybe set the texture coordinates at a vertex. OpenGL uses these commands to draw primitives of the type specified by the PrimitiveMode parameter.

In order to draw primitives with textures on them, texture coordinates must be given with each primitive vertex. The texture coordinates will tell HOpenGL which sections from the currently selected texture to draw on your primitive. The diagram below illustrates how this works.



In all of these examples, we are passing (x, y) texture coordinates along with each vertex of our primitive, which are Quads in these three cases (we'll get to how these coordinates are actually passed later). In Example A, we give (0.5, 0.0) to OpenGL along with vertex X, (1.0, 0.0) along with vertex Y, (0.5, 0.5) along with vertex Z, and (1.0, 0.5) with vertex W. The end result is a primitive drawn with a section of our original texture. If we wanted to draw our entire texture on the Quad, we would send (0.0, 0.0) with vertex X, (1.0, 0.0) with Y, (0.0, 1.0) with Z and (1.0, 1.0) with W.

Example B illustrates that the texture coordinates given to OpenGL do not have to scale proportionally with the primitive vertex coordinates. In this example, our Quad is the same size as the Quad in Example A, but we are drawing the lower half of our texture onto it instead of a single quadrant. This results in the texture looking squished.

Example C illustrates the same idea, only in reverse. In this example, we are drawing the same quadrant of the texture onto our Quad that we did in Example A, but we have stretched the Quad to twice its size in the y-direction. The result is that the texture is stretched as well.

We should make a few notes here:

The first is simply that there are no constraints on which sections of the texture can be given to OpenGL when drawing a primitive. In all of our examples, we took sections of the texture that were connected to one or more of its corners, but this is not mandatory in any way. For instance, passing the following texture coordinates with the verticies of our Quad would draw a section of the texture from the middle:

(0.25, 0.25) with X
(0.6, 0.25) withY
(0.25, 0.8) with Z
(0.6, 0.8) with W

The second note is that in our examples, we are taking rectangular sections of our texture and drawing them on Quads. In general, you can "cut out" a piece of the texture in any shape you like, not just rectangles.

The third note is that our texture coordinates go from 0.0 to 1.0 in each direction. Even though our texture is square, the texture coordinates will go from 0.0 to 1.0 for any OpenGL texture, even rectangular ones. Even though the coordinates go from 0.0 to 1.0, it is possible to ask OpenGL to "wrap" a texture around one or more of its edges. When a texture has been wrapped, you can pass texture coordinates that are bigger than 1.0 or less than 0.0. When this is done, the texture will appear to be tiled. This effect can be easily observed in many older games in which the texture for a large polygon is actually the same picture repeated infinitely.

The last note is that we dealt with 2D textures in our example. Even though we will not cover them in this tutorial, keep in mind that OpenGL supports 1D and 3D textures as well.

Drawing Sprites in HOpenGL : Sprites.hs

Sprites are flat, rectangular pictures. The Sprites module written by David Morra and Eric Etheridge provides methods for easily loading raw (.rgb) image files as textures and drawing sprites to the screen.

The Sprites module draws a sprite to the screen by rendering a Quad with the appropriate texture on it. For our purposes, the four verticies of the Quad will corrospond to the four corners of our sprite.

Because Quads are fully-fledged 3D objects in OpenGL, we can easily achieve the rotation and scaling effects that one would expect to find in a pure spriting engine. The Sprites module supports rotation of sprites, and scaling can be done by changing the coordinates of the sprite's corners.

Setting Up : The spriteInit Function

Before we will be able to properly draw sprites to the screen, we must first set a few OpenGL environment variables. These can be set individually in your Haskell program, but we have written a handy function which sets them all up automatically. It is called spriteInit:

spriteInit :: Int -> Int -> IO ()
spriteInit x y = do
blendEquation $= Just FuncAdd
blendFunc $= (SrcAlpha, OneMinusSrcAlpha)
textureFunction $= Replace
texture Texture2D $= Enabled
clearColor $= Color4 0.0 0.0 0.0 0.0
color (Color4 0.0 0.0 0.0 (1.0 :: GLfloat))
ortho 0.0 (fromIntegral x) 0.0 (fromIntegral y) (-1.0) (1.0)
The spriteInit function takes two Int's (discussed later) and is of type IO (). We will go through each of its lines independently, but first we should explain the operator ($=). This operator is a part of HOpenGL, and it is used to set values. Without going into deeper explanation, it should simply be thought of an assignment operator. It is there to make the HOpenGL parts of Haskell code resemble their imperative cousins as closely as possible.

Now, onto spriteInit. The first line, blendEquation $= Just FuncAdd, tells OpenGL that we wish to enable blending when we are drawing things to the screen. Having blending enabled will allow us to do things like have our background show through the transparent parts of our sprites. When we enable blending, we also tell OpenGL how to calculate the resultant color from a blending operation. In our case, we just want to add the colors.

Note: By the time you read this tutorial, this method of enabling blending may be obsolete. With the latest versions of HOpenGL, you may have to also include the line blend $= Enabled to turn blending on. This is noted in the source code for the Sprites module.

Once we have turned blending on, we want to tell OpenGL how to handle two colors which are being blended together. The second line handles this: blendFunc $= (SrcAlpha, OneMinusSrcAlpha). Without going into to much detail, this line makes colors blend like you would expect. If you draw a transparent or translucent sprite onto the screen, the colors that were already there will show through it.

When primitives are drawn to the screen, they can have both a color and a texture. The next line, textureFunction $= Replace, tells OpenGL to ignore the primitive's color and replace it entirely with the colors (and alpha values) contained in the texture. We do this so that our blending function will use the colors contained in the texture, and not in the Quad.

texture Texture2D $= Enabled tells OpenGL that we want to use 2D textures. A similar variable exists for turning on 1D and 3D textures, but we do not use them in our engine.

clearColor $= Color4 0.0 0.0 0.0 0.0 sets the clear color. The user may call a command to draw this color to every pixel on the screen. Generally, it does not matter what color you put here, but the alpha value should be 0.0.

The next line is: color (Color4 0.0 0.0 0.0 (1.0 :: GLfloat)). As we have said before, primitives which are drawn in OpenGL can have both a texture and a color. This line sets the color that all primitives will be drawn with. Since we are overwriting our primitives with our textures, we don't really care what this color is. It is a good idea to keep this color's alpha value as 1.0.

Now onto the last line. This line is here because of the way sprites have traditionally been drawn: ortho 0.0 (fromIntegral x) 0.0 (fromIntegral y) (-1.0) (1.0). For starters, we are putting OpenGL in orthographic mode (not perspective mode, which is used in games like first person shooters). By using orthographic mode, we ensure that objects will not become distorted near the edges of the screen. ortho's six arguments define the edges of the viewing volume. They go in this order: minimum x, maximum x, minimum y, maximum y, minimum z, maximum z. It should also be noted that we pass the two Int's which were passed to our spriteInit function to ortho as arguments.

We are using this function to define our screen's virtual pixel size. For instance, if we called spriteInit 800 600 we would wind up creating a screen which is 800x600 virtual pixels large. We say virtual pixels because the viewing volume is defined independently of the window size. That is to say, we can have an 800x600 display inside of a window with a size of 1024x768. All this means is that the pixel corrosponding to (0, 0) would be at (0, 0) in the window, and the pixel corrosponding to (800, 600) would be at pixel (1024, 768) in the window, and everything else would be stretched out inbetween.

This may seem like a weird way to do things. We do it this way because the sprite drawing functions in the Sprites module take Int's as arguments instead of floating point numbers. Traditionally, sprites have been discrete objects comprised of discrete parts. That is to say, the information held in sprites was stored as integral information, whereas OpenGL deals primarily in floating point information. To keep with the legacy of discrete storage and discrete display, we draw sprites as if we were drawing pixels to the screen, and we use the last line of spriteInit to say how many pixels we have to draw with.

Reading Images Into Memory With HOpenGL : pngtorgb and ReadImage.hs

Before we go on, we need to deal with the annoying task of loading our textures so that we can draw with them. The OpenGL API does not include built-in functions to read image files into memory, so it is up to the user to write the appropriate routines necessary to read image files and transform them into data that OpenGL can understand.

Loading textures using the Sprites module is a two step process. The first step involves using pngtorgb, an intermediate tool which transforms a png image to a raw color file which will be readable by Sven Panne's ReadImage module. After the image has been converted, it can then be read by the Sprite module's texture-loading functions. Since Haskell has not yet been recgonized by many game developers as a viable language, not many image loading solutions exist. It is quite likely that as Haskell gains mainstream recgonition that image loading libraries will be written for a variety of different image types.

pngtorgb is invoked from a command line like so:

> pngtorgb image.png

This program does not support multiple filenames or any regular expression identification. If invoked with multiple commands, it will ignore all but the last which it will take to be the filename of the png you wish to convert.

This program uses the libpng library to open and read a png file, writing it to an .rgb file with 8-bit RGBA color. The input png file must have alpha channel information, even if the entire image is opaque.

This program has only been compiled for *NIX platforms. The source code is provided and should easily compile on any platform supported by libpng. When compiling, don't forget to link libpng.

Texture Creation and Loading : createTexture and createTextures

The Sprites module uses two functions to load textures:

createTexture :: FilePath -> (Bool, Bool) -> IO (Maybe TextureObject)
createTextures :: [(FilePath, (Bool, Bool))] -> IO [(Maybe TextureObject)]

As the two function types suggest, createTextures simply calls createTexture on a list of arguments. Its definition is:

createTextures parameters = mapM (uncurry createTexture) parameters

The definition for createTexture is as follows:

createTexture :: FilePath -> (Bool, Bool) -> IO (Maybe TextureObject)
createTexture filename (repeatX, repeatY) = do
[texName] <- genObjectNames 1
textureBinding Texture2D $= Just texName
when repeatX (textureWrapMode Texture2D S $= (Repeated, Repeat))
when repeatY (textureWrapMode Texture2D T $= (Repeated, Repeat))
textureFilter Texture2D $= ((Nearest, Nothing), Nearest)
((Size x y), pixels) <- readImage filename
texImage2D Nothing NoProxy 0 RGBA' (TextureSize2D x y) 0 pixels
return (Just texName)
This function takes a path to the .rgb file you wish to load and two Bool's, which may be set to True if you wish to have your texture wrap in the x and/ or y directions. The first Bool is for the x component, the second for the y. This function gives back a (Maybe TextureObject), which may be passed to HOpenGL as a texture. We will quickly step through this function line by line:

[texName] <- genObjectNames 1
We call this function to generate our texture. In OpenGL, each texture is assigned a number. By calling this function, we ask OpenGL to assign our texture a unique identifier.

textureBinding Texture2D $= Just texName
This function is called to set our new texture as OpenGL's current texture. This ensures that all texture operations that occur from this point on will happen to this texture. This function may be called anytime you wish to change the current texture.

when repeatX (textureWrapMode Texture2D S $= (Repeated, Repeat))
when repeatY (textureWrapMode Texture2D T $= (Repeated, Repeat))
These two lines will tell HOpenGL whether or not you want your new texture to wrap in the S (x) or T (y) directions. These lines both use the when function. Its type is:

when :: Monad m => Bool -> m () -> m ()

The when function evaluates its second argument if its first argument is True and does nothing if its first argument is False. So in these two lines, it will set texture wrapping independently for the x and y directions depending upon the two Bool's passed into the function.

textureFilter Texture2D $= ((Nearest, Nothing), Nearest)
This line is necessary to tell OpenGL how to display our texture.

((Size x y), pixels) <- readImage filename
This line invokes readImage, a function from the ReadImage module, to read our .rgb file and convert it into an OpenGL-friendly form. The readImage function will return the integral size of the image, as well as an HOpenGL PixelData structure which is used in the next line.

texImage2D Nothing NoProxy 0 RGBA' (TextureSize2D x y) 0 pixels
We use the texImage2D function in this line to bind our image data to our texture. This function performs the binding for a 2D texture, and similar functions exist for 1D and 3D textures. This function takes many parameters, but for our purposes, we only care about 3 of them. Those are the RGBA' parameter, which tells OpenGL that our image has alpha data in addition to RGB color, the (TextureSize2D x y) parameter, which tells OpenGL the size of our image, and pixels, which passes along our image information. By calling this function, we are asking OpenGL to load this image into memory.

return (Just texName)
After we are done setting up our new TextureObject, we return it as a (Maybe TextureObject).

Be advised that the images you use as textures must have sizes (in both the x and y direction) which are powers of 2, even if the sizes in each direction are not the same. Examples of good sizes are 128x128, 512x64, 32x2048, etc.

Actually Drawing Sprites : displaySprite and displaySpriteWithFrame

After we are done setting everything up, we are finally ready to draw sprites. The Sprites module exports two functions with which the user may do this:

displaySprite :: Maybe TextureObject -> (Int, Int) -> (Int, Int) -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> GLfloat -> IO ()

displaySpriteWithFrame :: Maybe TextureObject -> (Int, Int) -> (Int, Int) -> (Int -> ((GLfloat, GLfloat), (GLfloat, GLfloat))) -> Int -> GLfloat -> IO ()

These are the only two sprite drawing functions which may be accessed from outside the Sprites module, but there are many more functions involved in drawing the sprites, including the function which actually does all the work, displaySpriteBackend. The types of the hidden functions are as follows:

displaySpriteBackend :: Maybe TextureObject -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> GLfloat -> IO ()

findCenterBackend :: (Int, Int) -> (Int, Int) -> (GLfloat, GLfloat)

findSizeBackend :: (Int, Int) -> (Int, Int) -> (GLfloat, GLfloat)

setVertex :: (TexCoord2 GLfloat, Vertex3 GLfloat) -> IO ()

mapVerticies :: [(TexCoord2 GLfloat)] -> [(Vertex3 GLfloat)] -> IO ()


We will begin by explaining the last four functions since they are the shortest and perform the simplest tasks. We will start with findCenterBackend and findSizeBackend, which are defined like this:

findCenterBackend :: (Int, Int) -> (Int, Int) -> (GLfloat, GLfloat)
findCenterBackend (x0, y0) (x1, y1) = (((fromIntegral (x1 - x0)) / 2) + (fromIntegral x0), ((fromIntegral (y1 - y0)) / 2) + (fromIntegral y0))


findSizeBackend :: (Int, Int) -> (Int, Int) -> (GLfloat, GLfloat)
findSizeBackend (x0, y0) (x1, y1) = ((fromIntegral (x1 - x0)) / 2, (fromIntegral (y1 - y0)) / 2)


These two functions are straightforward. findCenterBackend finds the center of the rectangle formed by the lower-left and upper-right corners defined by (x0, y0) and (x1, y1), and returns it as a pair of GLfloat's. findSizeBackend returns a GLfloat pair which holds two numbers, one for the x direction and one for the y direction. Each number is the size of the rectangle along their respective axis divided by 2.

The functions setVertex and mapVerticies are defined this way:

setVertex :: (TexCoord2 GLfloat, Vertex3 GLfloat) -> IO ()
setVertex (texCoordinates, vertexCoordinates) = do texCoord texCoordinates; vertex vertexCoordinates;


mapVerticies :: [(TexCoord2 GLfloat)] -> [(Vertex3 GLfloat)] -> IO ()
mapVerticies texs verts = mapM_ setVertex (zip texs verts)


setVertex is a monadic function which takes texture coordinates and primitive vertex coordinates and tells OpenGL to set those as the current coordinates. It calls two monadic functions, texCoord and vertex, to set the respective values. mapVerticies calls setVertex on a zipped list of texture coordinates and primitive vertex coordinates. Now, onto the juicy stuff:

displaySpriteBackend :: Maybe TextureObject -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> GLfloat -> IO ()
displaySpriteBackend image (cx, cy) (sx, sy) (tx0, ty0) (tx1, ty1) angle = do
textureBinding Texture2D $= image
preservingMatrix $ do
do translate $ Vector3 cx cy 0
do rotate angle $ Vector3 0 0 1
let verts = [(Vertex3 (-sx) (-sy) 0), (Vertex3 (-sx) (sy) 0), (Vertex3 (sx) (sy) 0), (Vertex3 (sx) (-sy) 0)]
let texs = [(TexCoord2 tx0 ty1), (TexCoord2 tx0 ty0), (TexCoord2 tx1 ty0), (TexCoord2 tx1 ty1)]
renderPrimitive Quads $ do mapVerticies texs verts
This function finally does the work of drawing our sprite. It takes a (Maybe TextureObject), four pairs of GLfloat's, and a final GLfloat defining rotation. The first pair of GLfloat's defines the center of our sprite, and the second pair defines the half-lengths of the rectangle along the x and y axis. The last two pairs define the upper-left and lower-right corners of the area on the texture we wish to copy onto our Quad. We'll go through displaySpriteBackend line by line:

textureBinding Texture2D $= image
We called the textureBinding function earlier when we were creating our textures. Here we are calling it again to set image as our current texture so that we may draw from it.

preservingMatrix $ do
OpenGL uses matrix math to draw things to the screen. This line tells OpenGL that we want to evaluate the following functions with a new matrix, but while preserving our original one. We do this because the translation and rotation matricies for each sprite will be different, but they are all displayed using the same matrix.

do translate $ Vector3 cx cy 0
do rotate angle $ Vector3 0 0 1

These lines set up the translation and rotation matricies for our sprite. The translate function sets the origin to be the center of our sprite, and the rotate function rotates our coordinate system by the angle we passed into displaySpriteBackend. These functions assume two things. The first is that all sprites are being drawn with a z coordinate of 0.0. Since sprites are inherantly 2D objects, we can simply ignore anything in the z direction even though OpenGL asks for 3D coordinates. Since we wish to rotate our sprites solely in the x-y plane, we use the vector <0, 0, 1> with the rotation function.

let verts = [(Vertex3 (-sx) (-sy) 0), (Vertex3 (-sx) (sy) 0), (Vertex3 (sx) (sy) 0), (Vertex3 (sx) (-sy) 0)]
let texs = [(TexCoord2 tx0 ty1), (TexCoord2 tx0 ty0), (TexCoord2 tx1 ty0), (TexCoord2 tx1 ty1)]

These two lines will define the vertex coordinates of our Quad as well as the associated texture coordinates for each vertex. These two lists are passed into the final line of displaySriteBackend:

renderPrimitive Quads $ do mapVerticies texs verts
Here, we finally call renderPrimitive to draw our sprite to the screen, passing mapVerticies texs verts as the commands we would like to execute.

Now, we can finally explain the two sprite drawing functions exported from the Sprites module:

displaySprite :: Maybe TextureObject -> (Int, Int) -> (Int, Int) -> (GLfloat, GLfloat) -> (GLfloat, GLfloat) -> GLfloat -> IO ()
displaySprite image min max texMin texMax angle = displaySpriteBackend image (findCenterBackend min max) (findSizeBackend min max) texMin texMax angle

displaySpriteWithFrame :: Maybe TextureObject -> (Int, Int) -> (Int, Int) -> (Int -> ((GLfloat, GLfloat), (GLfloat, GLfloat))) -> Int -> GLfloat -> IO ()
displaySpriteWithFrame image min max func frame angle = displaySpriteBackend image (findCenterBackend min max) (findSizeBackend min max) texMin texMax angle
where (texMin, texMax) = func frame
Both of these functions take a (Maybe TextureObject), which is the texture to draw from, and two pairs of Int's, which respectively define the lower-left and upper-right hand corner of the sprite we wish to draw. Both of these functions will call findCenterBackend and findSizeBackend on the Int pairs and pass the results into displaySpriteBackend. These functions differ in how they handle texture coordinates, however. displaySprite takes two pairs of GLfloat's and simply pass them on to displaySpriteBackend. displaySpriteWithFrame is different, however. Instead of pairs of GLfloat's, it takes a function which takes an Int and gives back two pairs of GLfloats, and an Int to evaluate the function at. This functionality is provided as an easy way to draw frames of animation without having to keep track of anything other than the frame number.

displaySpriteWithFrame's usefulness is illustrated with the following example:


If I wanted to animate this cursor, I could number its four frames of animation 0 - 3 from left to right, and then write this function:

getCursorCoordinates :: Int -> ((GLfloat, GLfloat), (GLfloat, GLfloat))
getCursorCoordinates frameNumber = (((fromIntegral frameNumber) * (1 / 4), 0.0), ((fromIntegral frameNumber) * (1 / 4) + (1 / 4), 1.0))


My function, getCursorCoordinates would take a frame number and give back the appropriate part of the sprite to draw as a pair of GLfloat pairs. All I then have to do to display a frame of animation for my cursors is by keeping around a value containing the frame number (let's call it frame), and invoking this function:

displaySpriteWithFrame cursors (minX, minY) (maxX, maxY) getCursorCoordinates frame cursorAngle

Just one final note to make: OpenGL considered the lower left hand corner of the screen to be the minimum x and y coordinates, but with textures, the minimum coordinates are at the upper-left coordinate of the texture. It is necessary to keep this in mind when writing functions involving texture coordinates to ensure that things get displayed correctly.

The Sprites Module in Action : SpritingDemo.hs

We have provided an example program which draws simple sprites to the screen using the Sprites module. It is comprised of five functions:

main :: IO ()

display :: IORef GLfloat -> IORef Int -> (Maybe TextureObject, Maybe TextureObject, Maybe TextureObject, Maybe TextureObject) -> IO ()

getCursorsFrame :: Int -> ((GLfloat, GLfloat), (GLfloat, GLfloat))

makeNewWindow :: String -> IO ()

timer :: IORef GLfloat -> IORef Int -> (Maybe TextureObject, Maybe TextureObject, Maybe TextureObject, Maybe TextureObject) -> IO ()


We will explain these functions in order of simplest to most complex, and finish with main, which ties everything together. We'll start with getCursorFrame:

getCursorsFrame :: Int -> ((GLfloat, GLfloat), (GLfloat, GLfloat))
getCursorsFrame frame
| frame < 3 = (((fromIntegral frame) * (1 / 4), 0), ((fromIntegral frame) * (1 / 4) + (1 / 4), 1))
| frame >= 3 = (((3 / 4) - (1 / 4) * ((fromIntegral frame) - 3), 0), (1 - (1 / 4) * ((fromIntegral frame) - 3), 1))
This function is similar to the function we wrote above to animate our cursor. getCursorFrame is used to animate the same cursors picture, but this time we wish to have 6 frames. Frames numbers 0 - 3 are the same as we defined them above, but we are adding frames 4 and 5 in order to play a smooth animation. Frame 4 is the same as frame 2 and frame 5 is the same as frame 1. We do this so that we may play the frames in a cycle (0, 1, 2, 3, 4, 5, 0, 1, 2, etc) and get a smooth animation.

The function makeNewWindow is a routine we call to set up our environment and create the window that our application will run in:

makeNewWindow :: String -> IO ()
makeNewWindow name = do
createWindow name
windowSize $= Size 800 600
spriteInit 800 600
clearColor $= Color4 0.2 0.3 0.6 0.0
[tex1, tex2, tex3, cursors] <- createTextures [("test6.rgb", (False, False)), ("test4.rgb", (False, False)), ("test5.rgb", (False, False)), ("cursors.rgb", (True, True))]
angle <- newIORef (0 :: GLfloat)
frame <- newIORef (0 :: Int)
displayCallback $= display angle frame (tex1, tex2, tex3, cursors)
addTimerCallback msInterval (timer angle frame (tex1, tex2, tex3, cursors))
createWindow name
This is an HOpenGL function which takes care of creating a window for us. It will set name as the window title.

windowSize $= Size 800 600
spriteInit 800 600

These two lines set up the properties of our environment. The windowSize variable will hold the physical dimensions of our window. In this case, we are creating an 800x600 window. The second line is the spriteInit function we discussed earlier. In this case, we are creating a window with the same size as the virtual pixel environment created by spriteInit. It should be noted that these two sizes do not have to necessarily be the same. People with low-resolution displays may want to change the values given to windowSize in order to create a smaller window. In any case, the arguments to spriteInit should be left alone.

clearColor $= Color4 0.2 0.3 0.6 0.0
Even though spriteInit initializes the clear color to black, we are changing it to a different color here for the purpose of demonstrating sprite transparency/ translucency. This line can be commented out with no problems if desired.

[tex1, tex2, tex3, cursors] <- createTextures [("test6.rgb", (False, False)), ("test4.rgb", (False, False)), ("test5.rgb", (False, False)), ("cursors.rgb", (True, True))]
Here we call the createTextures function on a list of FilePath's and Bool pairs to create four textures.

angle <- newIORef (0 :: GLfloat)
frame <- newIORef (0 :: Int)

These two lines create IORef's that will store data pertaining to the rotation of our sprites, as well as the animation frame for our cursor.

displayCallback $= display angle frame (tex1, tex2, tex3, cursors)
addTimerCallback msInterval (timer angle frame (tex1, tex2, tex3, cursors))

These two lines define callback functions. The displayCallback variable holds a function which is called after our window is initialized and is ready to display data. In our case, we are assigning a function called display to be our display callback. The function addTimerCallback sets up a timer callback function which will be called after a certain number of milliseconds have passed. addTimerCallback's first argument is the millisecond count, and we give it an Int called msInterval which is defined at the top of SpritingDemo.hs. The timer callback function is called timer.

Callback functions are of type IO (), so in our callback creation we evaluate display and timer fully. Here is the definition of timer:

timer :: IORef GLfloat -> IORef Int -> (Maybe TextureObject, Maybe TextureObject, Maybe TextureObject, Maybe TextureObject) -> IO ()
timer angle frame textures = do
modifyIORef angle (\num -> if (num + rotSpeed >= 360.0) then (360 - (num + rotSpeed)) else (num + rotSpeed))
modifyIORef frame (\f -> if (f == 5) then 0 else (f + 1))
display angle frame textures
addTimerCallback msInterval (timer angle frame textures)
The first two lines of this function update the two IORef's we created in makeNewWindow. We wish for the information stored in angle to always be a number between 0.0 and 360.0, and we want the information in frame to always be between 0 and 5. The first line makes use of a rotation speed number called rotSpeed which is defined in the first few lines of SpriteDemo.hs.

The next line, display angle frame textures, calls our display function with the information passed to timer.

The final line adds a new timer callback. In OpenGL, a timer callback vanishes after it is executed, so we must continually set new callbacks to keep the program going.

Now, we are ready to delve into the display function:

display :: IORef GLfloat -> IORef Int -> (Maybe TextureObject, Maybe TextureObject, Maybe TextureObject, Maybe TextureObject) -> IO ()
display angle frame (tex1, tex2, tex3, cursors) = do
clear [ColorBuffer, DepthBuffer]
angle' <- readIORef angle
frame' <- readIORef frame
displaySprite tex1 (304, 304) (560, 560) (0, 0) (1, 1) angle'
displaySprite tex2 (160, 160) (288, 288) (0, 0) (1, 1) (-angle')
displaySprite tex3 (80, 80) (144, 144) (0, 0) (1, 1) angle'
displaySprite tex1 (32, 32) (64, 64) (0, 0) (1, 1) (-angle')
displaySprite tex2 (0, 0) (16, 16) (0, 0) (1, 1) angle'
displaySpriteWithFrame cursors (600, 200) (664, 264) getCursorsFrame frame' 0
flush
swapBuffers
This function is very mechanical and straightforward. The first line clears the screen using the clear color we defined earlier. It also clears the depth buffer, but we won't discuss that in this tutorial.

angle' <- readIORef angle
frame' <- readIORef frame

These two lines read our two IORef's and store the information in a GLfloat called angle' and an Int called frame'. These will be used in the next few lines:

displaySprite tex1 (304, 304) (560, 560) (0, 0) (1, 1) angle'
displaySprite tex2 (160, 160) (288, 288) (0, 0) (1, 1) (-angle')
displaySprite tex3 (80, 80) (144, 144) (0, 0) (1, 1) angle'
displaySprite tex1 (32, 32) (64, 64) (0, 0) (1, 1) (-angle')
displaySprite tex2 (0, 0) (16, 16) (0, 0) (1, 1) angle'
displaySpriteWithFrame cursors (600, 200) (664, 264) getCursorsFrame frame' 0

Here we call our two sprite drawing functions a bunch of times with different parameters to draw all of our sprites on the screen. Note that the last line invokes displaySpriteWithFrame into which we pass our cursor animation function getCursorsFrame and frame'.

flush
swapBuffers

These last two lines are just mechanical. flush tells OpenGL to perform all of the drawing operations defined up to this point. In this demo, we are using double buffering. This means that all drawing operations happen on a piece of memory which is hidden from the screen. Once the function swapBuffers is called, that piece is memory becomes visible and the memory which was just displayed becomes hidden so that it may be drawn to. Double buffering is a technique that is used to avoid flicker while drawing.

Now that we have defined all of our functions, it is time to explain main line by line.

main = do
(progName, _) <- getArgsAndInitialize
initialDisplayMode $= [DoubleBuffered, RGBAMode, WithDepthBuffer, WithAlphaComponent]
makeNewWindow "HOpenGL Spriting Demo"
mainLoop
(progName, _) <- getArgsAndInitialize
Calling getArgsAndInitialize will set up HOpenGL. The function will spit out the name of the program and some other information which we can ignore. If we wanted to, we could pass progName into makeNewWindow as the window name. In this example, we are explicitly naming the progName value, even though we do not use it in the program.

initialDisplayMode $= [DoubleBuffered, RGBAMode, WithDepthBuffer, WithAlphaComponent]
This line tells OpenGL that we want to use double buffering, RGBA color, we want to have a depth buffer, and we want our colors to contain alpha information.

makeNewWindow "HOpenGL Spriting Demo"
We call the makeNewWindow function we defined earlier. We are going to call our window "HOpenGL Spriting Demo".

mainLoop
This line tells OpenGL to enter its loop and wait for callbacks to be triggered. This function has to be called in every HOpenGL program.

The End : Compiling and Running the Programs

We have gone through and explained every last line in the Sprites module and our spriting demo. If you would like to compile the source code for these programs, it is freely available. The source code archive you can download includes scripts that you may run to compile these programs automatically (called buildSpritingDemo and buildPngToRgb).

So far, these programs have only been compiled on Debian GNU/Linux (using the ghc-cvs and ghc-cvs-hopengl binary packages), but since the source code contains nothing Linux-specific, the spriting demo and pngtorgb should compile on any platform which is supported by HOpenGL and libpng. If anyone succsessfully compiles one or both programs on a Windows or Mac machine, we would appreciate some feedback. It is also pretty likely that future versions of HOpenGL and/or GHC may break parts of these programs. If this happens, we would appreciate a heads-up so that we can try to keep them up-to-date.

Special thanks to Sven Panne for all of his hard work developing the HOpenGL API, and for writing the ReadImage module used by our Sprites module.

Special thanks to haskell.org for hosting this tutorial and source code.

The Sources : PngToRgb.tar.gz and SpritingDemo.tar.gz

PngToRgb.tar.gz

SpritingDemo.tar.gz

This source code for the Sprites module, the spriting demo, and the png conversion program is copyright 2005 by David Morra and Eric Etheridge. The ReadImage module was written by Sven Panne but has been slightly modified. The details are contained in the copyright files in each of the archives, but basically you are free to use, copy, redistribute, alter, compile, integrate, and do whatever to this code you feel like doing as long as we're not held responsible for the consequences.

One final note: As of the publication of this tutorial, the Sprites module has undergone a huge overhaul. New features have been added including native scaling, rotation about an arbitrary point and even corner manipulation. These are things that one should expect in a fully functional spriting engine, plus more. I hope to write a tutorial for the new module soon.

Thanks for reading and good luck.