|
OpenGL is a very powerful graphics library that is more commonly used for 3D graphics, but what about 2D graphics? Well there is little to no documentation on how to use it for a pure 2D application. This is exactly where this article comes-in.
Strugelling to get openGL to render simple Microsoft Bitmap images, I came up with 2 different classes to do just that. An important thing to understand is that there are 2 separate parts to this process, the first is reading in an image file and decoding it into a 2D matrix containing color information (RGB, RGBA, etc). To do that we can write our own class, as I did here, or we can use a library to do it for us. Using a library is a much better option for production application because, it makes it easier to support multiple file formats. Once such library is Magic++, which has rich support for multiple formats and allows you to do all sorts of image manipulation. The second step -- which is the focus of this article -- is to display the image with OpenGL. We will use textures to do that, and add a few other useful operation to a class so it can be easily reused. First of is the BitmapImage class, used to read a bitmap file into memory and store the meta-data found in it to provide the rest of the application with access to this data. This is what it looks like: ImageLoader.h: /*
* ImageLoader.h
*
* An image loader designed to read a 32-bit bitmap. Although it may be extended to be
* do other things too in the future.
*
* Created on: 2010-08-11
* Author: Michael Yagudaev
* Copyright: yagudaev.com
* Version: $0.1.0$
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License Version 3 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
#ifndef IMAGELOADER_H_
#define IMAGELOADER_H_
typedef unsigned char BYTE;
typedef int LONG;
typedef unsigned int DWORD;
typedef unsigned short WORD;
//File information header
//provides general information about the file
typedef struct __attribute__ ((__packed__)) tagBITMAPFILEHEADER
{
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;
//Bitmap information header
//provides information specific to the image data
typedef struct __attribute__ ((__packed__)) tagBITMAPINFOHEADER
{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
DWORD biXPelsPerMeter;
DWORD biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;
//Colour palette
typedef struct __attribute__ ((__packed__)) tagRGBQUAD
{
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
class ImageLoader
{
public:
//variables
//methods
/**
* Initializes an empty image
*/
ImageLoader(void);
/**
* Initializes an image with the given image loaded from disk
*/
ImageLoader(const char *fileName);
/**
* Destructor...
*/
virtual ~ImageLoader();
/**
* Loads the given image
*/
bool loadBMP(const char *fileName);
/**
* Get the alpha channel as an array of bytes
* @param size The size of the returned array, will return -1 on failure
*/
BYTE *getAlpha() const;
// Getter and setters...
LONG getHeight() const
{
return height;
}
RGBQUAD *getColors() const
{
return colors;
}
bool getLoaded() const
{
return loaded;
}
BYTE *getPixelData() const
{
return pixelData;
}
LONG getWidth() const
{
return width;
}
private:
//variables
BITMAPFILEHEADER bmfh;
BITMAPINFOHEADER bmih;
RGBQUAD *colors;
BYTE *pixelData;
bool loaded;
LONG width;
LONG height;
WORD bpp;
//methods
void reset(void);
bool fixPadding(BYTE const * const tempPixelData, DWORD size);
};
#endif /* IMAGELOADER_H_ */
ImageLoader.cpp:
/*
* ImageLoader.cpp
*
* Created on: 2010-08-11
* Author: Michael Yagudaev
* Copyright: yagudaev.com
* Version: $0.1.1$
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License Version 3 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
#include "ImageLoader.h"
#include <cstdio>
#include <cstring>
#include <errno.h>
#define BITMAP_TYPE 19778
ImageLoader::ImageLoader()
{
reset();
}
ImageLoader::ImageLoader(const char *fileName)
{
reset();
loadBMP(fileName);
}
ImageLoader::~ImageLoader()
{
if(colors != NULL)
{
delete[] colors;
}
if(pixelData != NULL)
{
delete[] pixelData;
}
}
bool ImageLoader::loadBMP(const char * fileName)
{
FILE *in = NULL;
bool result = false;
//open the file for reading in binary mode
in = fopen(fileName, "rb");
if(in == NULL)
{
perror("Error");
printf("errno = %d\n", errno);
return false;
}
fread(&bmfh, sizeof(BITMAPFILEHEADER), 1, in);
// check if this is even the right type of file
if(bmfh.bfType != BITMAP_TYPE)
{
perror("Error");
printf("errno = %d\n", errno);
return false;
}
fread(&bmih,sizeof(BITMAPINFOHEADER),1,in);
width = bmih.biWidth;
height = bmih.biHeight;
bpp = bmih.biBitCount;
// TODO: make this work for 24bit images as well! It will segfault if given a 24 bit image...
//set the number of colours
LONG numColors = 1 << bmih.biBitCount;
//bitmap is not loaded yet
loaded = false;
//make sure memory is not lost
if(colors != NULL)
{
delete[] colors;
}
if(pixelData != NULL)
{
delete[] pixelData;
}
//load the palette for 8 bits per pixel
if(bmih.biBitCount < 24)
{
colors = new RGBQUAD[numColors];
fread(colors, sizeof(RGBQUAD), numColors, in);
}
DWORD size = bmfh.bfSize - bmfh.bfOffBits;
BYTE *tempPixelData = NULL;
tempPixelData = new BYTE[size];
if(tempPixelData == NULL)
{
printf("Error: out of memory. Cannot find space to load image into memory.\n");
fclose(in);
return false;
}
fread(tempPixelData, sizeof(BYTE), size, in);
result = fixPadding(tempPixelData, size);
loaded = result;
delete[] tempPixelData;
fclose(in);
return result;
}
bool ImageLoader::fixPadding(BYTE const * const tempPixelData, DWORD size)
{
//byteWidth is the width of the actual image in bytes
//padWidth is the width of the image plus the extra padding
LONG byteWidth, padWidth;
//initially set both to the width of the image
byteWidth = padWidth = (LONG)((float)width * (float)bpp / 8.0);
//add any extra space to bring each line to a DWORD boundary
short padding = padWidth % 4 != 0;
padWidth += padding;
DWORD diff;
int offset;
height = bmih.biHeight;
//set diff to the actual image size(no padding)
diff = height * byteWidth;
//allocate memory for the image
pixelData = new BYTE[diff];
if(pixelData == NULL)
{
return false;
}
//bitmap is inverted, so the padding needs to be removed
//and the image reversed
//Here you can start from the back of the file or the front,
//after the header. The only problem is that some programs
//will pad not only the data, but also the file size to
//be divisible by 4 bytes.
if(height > 0)
{
offset = padWidth - byteWidth;
for(unsigned int i = 0; i < size - 2; i += 4)
{
if( (i + 1) % padWidth == 0)
{
i += offset;
}
// swap data for it to have the right order
*(pixelData + i) = *(tempPixelData + i + 2); // R
*(pixelData + i + 1 ) = *(tempPixelData + i + 1); // G
*(pixelData + i + 2) = *(tempPixelData + i); // B
*(pixelData + i + 3) = *(tempPixelData + i + 3); // A
}
}
//the image is not reversed. Only the padding needs to be removed.
else
{
// TODO: test this branch!
height = height * -1;
offset = 0;
do
{
memcpy((pixelData + (offset * byteWidth)), (tempPixelData + (offset * padWidth)), byteWidth);
offset++;
} while(offset < height);
}
return true;
}
void ImageLoader::reset(void)
{
width = 0;
height = 0;
pixelData = NULL;
colors = NULL;
loaded = false;
}
BYTE *ImageLoader::getAlpha() const
{
LONG arraySize = width * height;
BYTE *array = new BYTE[arraySize];
if(array == NULL)
{
delete[] array;
return NULL;
}
for(long i = 0; i < arraySize; i++)
{
array[i] = pixelData[i * 4 + 3]; // jump to the alpha and extract it everytime
}
return array;
}
Next is the sprite class which actually renders the 2D color matrix that the ImageLoader class retrieves from the bitmap file. It looks like so: Sprite.h
/*
* Sprite.h
*
* Created on: 2010-08-16
* Author: Michael Yagudaev
* Copyright: yagudaev.com
* version: $0.1.0$
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License Version 3 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
#ifndef SPRITE_H_
#define SPRITE_H_
#include <gl/glut.h>
#include <string>
using namespace std;
class ImageLoader;
class Sprite
{
public:
/**
* Enable 2D drawing mode to draw our sprites. This function MUST be called before
* any sprite is drawn on screen using the draw method.
*/
static void enable2D();
/**
* Disables the 2D drawing. This can be called before you are done drawing all 2D
* sprites and want to draw 3D now.
*/
static void disable2D();
Sprite(string filename);
virtual ~Sprite();
virtual void draw();
virtual void rotate(GLint degrees);
// getter and setter methods
GLint getAngle() const;
void setAngle(GLint degrees);
void setX(GLdouble x);
void setY(GLdouble y);
GLint getHeight() const;
GLint getWidth() const;
/**
* Sets the pivot point in relation to the sprite itself, that is using the object
* coordiantes system. In this coordiantes system the bottom left point of the object
* is at (0, 0) and the top right is at (1, 1).
*
* E.g. to set the pivot to be in the middle of the sprite use (0.5, 0.5)
* Default values are (1, 1).
* @param pivotX Can be any value, but when x is in the range [0, 1] the pivot is inside the
* sprite where 0 is the left edge of the sprite and 1 is the right edge of the sprite.
* @param pivotY Can be any value, but when y is in the range [0, 1] the pivot is inside the
* sprite where 0 is the bottom edge of the sprite and 1 is the top edge of the sprite.
*/
void setPivot(GLfloat pivotX, GLfloat pivotY);
GLfloat getPivotX() const;
GLfloat getPivotY() const;
GLdouble getX() const;
GLdouble getY() const;
/**
* Sets the pivot to be at the point where object's pivot is set.
* @param obj The reference object to whose pivot we will set this pivot to be.
* Note: if the obj pivot changes or the obj moves after the setPivot call has
* been issued, the pivot of this object will not reflect this changes. You must
* call setPivot again with that object to update the pivot information.
*/
void setPivot(const Sprite &obj);
/**
* Sets the scale of the object. A scale of (1.0, 1.0) means the sprite
* maintains its original size. Values larger than 1 scale the sprite up
* while values less than 1 shrink it down.
*/
void setScale(GLfloat x, GLfloat y);
private:
ImageLoader *image;
GLuint textureID;
GLint angle;
GLdouble x;
GLdouble y;
GLfloat pivotX;
GLfloat pivotY;
GLfloat scaleX;
GLfloat scaleY;
//-----------------------------------------------------------------------------
// Initializes extensions, textures, render states, etc. before rendering
//-----------------------------------------------------------------------------
void initScene();
/**
* A helper function taken from http://www.opengl.org/resources/features/OGLextensions/
* to help determine if an OpenGL extension is supported on the target machine at run-time
* @param extension The extension name as a string.
* @return True if extension is supported and false if it is not.
*/
bool isExtensionSupported(const char *extension) const;
};
#endif /* SPRITE_H_ */
Sprite.cpp
/*
* Sprite.cpp
*
* Created on: 2010-08-16
* Author: Michael Yagudaev
* Copyright: yagudaev.com
* version: $0.1.1$
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License Version 3 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
#include <GL/glut.h>
#include <iostream>
#include <cstring>
#include "Sprite.h"
#include "ImageLoader.h"
///////////////////////////////////////////////////////////////////////////////
// implementation is based on this article:
// http://www.gamedev.net/reference/articles/article2429.asp
///////////////////////////////////////////////////////////////////////////////
Sprite::Sprite(string filename)
{
image = new ImageLoader(filename.c_str());
angle = 0;
x = 0.0;
y = 0.0;
setPivot(0.0, 0.0);
setScale(1.0, 1.0);
}
Sprite::~Sprite()
{
delete image;
}
void Sprite::rotate(GLint degrees)
{
angle += degrees;
}
void Sprite::setAngle(GLint angle)
{
this->angle = angle;
}
GLint Sprite::getAngle() const
{
return angle;
}
void Sprite::enable2D()
{
GLint iViewport[4];
// Get a copy of the viewport
glGetIntegerv( GL_VIEWPORT, iViewport );
// Save a copy of the projection matrix so that we can restore it
// when it's time to do 3D rendering again.
glMatrixMode( GL_PROJECTION );
glPushMatrix();
glLoadIdentity();
// Set up the orthographic projection
glOrtho( iViewport[0], iViewport[0] + iViewport[2],
iViewport[1] + iViewport[3], iViewport[1], -1, 1 );
glMatrixMode( GL_MODELVIEW );
glPushMatrix();
glLoadIdentity();
// Make sure depth testing and lighting are disabled for 2D rendering until
// we are finished rendering in 2D
glPushAttrib( GL_DEPTH_BUFFER_BIT | GL_LIGHTING_BIT );
glDisable( GL_DEPTH_TEST );
glDisable( GL_LIGHTING );
}
void Sprite::disable2D()
{
glPopAttrib();
glMatrixMode( GL_PROJECTION );
glPopMatrix();
glMatrixMode( GL_MODELVIEW );
glPopMatrix();
}
void Sprite::initScene()
{
// Disable lighting
glDisable( GL_LIGHTING );
// Disable dithering
glDisable( GL_DITHER );
// Disable blending (for now)
glDisable( GL_BLEND );
// Disable depth testing
glDisable( GL_DEPTH_TEST );
// Is the extension supported on this driver/card?
if( !isExtensionSupported( "GL_ARB_texture_rectangle" ) )
{
cout << "ERROR: Texture rectangles not supported on this video card!" << endl;
exit(-1);
}
// NOTE: If your comp doesn't support GL_NV_texture_rectangle, you can try
// using GL_EXT_texture_rectangle if you want, it should work fine.
// Enable the texture rectangle extension
glEnable( GL_TEXTURE_RECTANGLE_ARB );
// Generate one texture ID
glGenTextures( 1, &textureID );
// Bind the texture using GL_TEXTURE_RECTANGLE_NV
glBindTexture( GL_TEXTURE_RECTANGLE_ARB, textureID );
// Enable bilinear filtering on this texture
glTexParameteri( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
// Write the 32-bit RGBA texture buffer to video memory
glTexImage2D( GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGBA, image->getWidth(), image->getHeight(),
0, GL_RGBA, GL_UNSIGNED_BYTE, image->getPixelData() );
}
GLfloat Sprite::getPivotX() const
{
return pivotX;
}
GLfloat Sprite::getPivotY() const
{
return pivotY;
}
void Sprite::setPivot(GLfloat pivotX, GLfloat pivotY)
{
GLfloat deltaPivotX = pivotX - getPivotX();
GLfloat deltaPivotY = pivotY - getPivotY();
this->pivotX = pivotX;
this->pivotY = pivotY;
x += deltaPivotX * image->getWidth();
y += deltaPivotY * image->getHeight();
}
void Sprite::setPivot(const Sprite &obj)
{
GLint worldX; // this x location if pivot was at setPivot(0, 0)
GLint worldY; // this y location if pivot was at setPivot(0, 0)
GLfloat newPivotX;
GLfloat newPivotY;
worldX = x - getPivotX() * image->getWidth();
worldY = y - getPivotY() * image->getHeight();
newPivotX = (float)(obj.x - worldX) / image->getWidth();
newPivotY = (float)(obj.y - worldY) / image->getHeight();
setPivot(newPivotX, newPivotY);
}
bool Sprite::isExtensionSupported(const char *extension) const
{
const GLubyte *extensions = NULL;
const GLubyte *start;
GLubyte *where, *terminator;
/* Extension names should not have spaces. */
where = (GLubyte *) strchr(extension, ' ');
if (where || *extension == '\0')
{
return false;
}
extensions = glGetString(GL_EXTENSIONS);
/* It takes a bit of care to be fool-proof about parsing the
OpenGL extensions string. Don't be fooled by sub-strings,
etc. */
start = extensions;
for (;;)
{
where = (GLubyte *) strstr((const char *) start, extension);
if (!where)
{
break;
}
terminator = where + strlen(extension);
if (where == start || *(where - 1) == ' ')
{
if (*terminator == ' ' || *terminator == '\0')
{
return true;
}
}
start = terminator;
}
return false;
}
void Sprite::draw()
{
initScene();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_TEXTURE_2D);
// Set the primitive color to white
glColor3f(1.0f, 1.0f, 1.0f);
// Bind the texture to the polygons
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, textureID);
glPushMatrix();
GLfloat transX = 1;
GLfloat transY = 1;
if(x != 0.0)
{
transX = x;
}
if(y != 0.0)
{
transY = y;
}
glLoadIdentity();
glTranslatef(transX, transY, 0);
glScalef(scaleX, scaleY, 1.0);
glRotatef(angle, 0.0, 0.0, 1.0);
// Render a quad
// Instead of the using (s,t) coordinates, with the GL_NV_texture_rectangle
// extension, you need to use the actual dimensions of the texture.
// This makes using 2D sprites for games and emulators much easier now
// that you won't have to convert :)
//
// convert the coordinates so that the bottom left corner changes to
// (0, 0) -> (1, 1) and the top right corner changes from (1, 1) -> (0, 0)
// we will use this new coordinate system to calculate the location of the sprite
// in the world coordinates to do the rotation and scaling. This mapping is done in
// order to make implementation simpler in this class and let the caller keep using
// the standard OpenGL coordinates system (bottom left corner at (0, 0))
glBegin(GL_QUADS);
glTexCoord2i(0, 0);
glVertex2i(-pivotX * image->getWidth(), -pivotY * image->getHeight());
glTexCoord2i(0, image->getHeight());
glVertex2i(-pivotX * image->getWidth(), (1 - pivotY) * image->getHeight());
glTexCoord2i(image->getWidth(), image->getHeight());
glVertex2i( (1 - pivotX) * image->getWidth(), (1 - pivotY) * image->getHeight());
glTexCoord2i(image->getWidth(), 0);
glVertex2i( (1 - pivotX) * image->getWidth(), -pivotY * image->getHeight());
glEnd();
glPopMatrix();
}
void Sprite::setX(GLdouble x)
{
this->x = x;
}
void Sprite::setY(GLdouble y)
{
this->y = y;
}
void Sprite::setScale(GLfloat x, GLfloat y)
{
scaleX = x;
scaleY = y;
}
GLint Sprite::getHeight() const
{
return image->getHeight() * scaleY;
}
GLint Sprite::getWidth() const
{
return image->getWidth() * scaleX;
}
GLdouble Sprite::getX() const
{
return x;
}
GLdouble Sprite::getY() const
{
return y;
}
Download the sprite and image loader classes here. ExampleThis calls for a simple yet realistic example. Let us create an analog clock (similar to what I had done here in flash) using these sprit classes to see how it works. All we need to do is to find a few good images of a clock face and clock hands and write a main source file to use these images. You can see the result on the right side and the code below. 
| | Figure 1 - Using the sprite class to create an analog clock |
/*
* main.c
*
* Example of using the sprite class to create an Analog Clock.
*
* Created on: 2010-12-26
* Author: Michael Yagudaev
* Copyright: yagudaev.com
* Version: $0.1.0$
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License Version 3 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
#include <GL/glut.h>
#include <cstdlib>
#include <string>
#include <iostream>
#include <sstream>
#include <time.h>
#include "Sprite.h"
#define ESCAPE_KEY 27
using namespace std;
static const time_t TIME_INTERVAL = 1; // in seconds
static int windowWidth = 524;
static int windowHeight = 524;
static Sprite *clockFace = NULL;
static Sprite *hoursHand = NULL;
static Sprite *minutesHand = NULL;
static Sprite *secondsHand = NULL;
void display (void)
{
glClear(GL_COLOR_BUFFER_BIT);
glRasterPos2i(0, 0);
// draw the clock
clockFace->setPivot(0.5, 0.5);
clockFace->setX(0);
clockFace->setY(0);
clockFace->draw();
hoursHand->setX(0);
hoursHand->setY(0);
hoursHand->setPivot(0.5, 0.075);
hoursHand->draw();
minutesHand->setX(0);
minutesHand->setY(0);
minutesHand->setPivot(0.5, 0.0566);
minutesHand->draw();
secondsHand->setX(0);
secondsHand->setY(0);
secondsHand->setPivot(0.5, 0.0545);
secondsHand->draw();
glFlush();
glutSwapBuffers();
glDisable(GL_TEXTURE_2D);
}
void reshape(int w, int h)
{
windowWidth = w;
windowHeight = h;
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-w/2, w/2, -h/2, h/2, -1.0, 1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void init (void)
{
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor(1.0, 1.0, 1.0, 0.0);
glShadeModel(GL_FLAT);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
Sprite::enable2D();
clockFace = new Sprite("graphics/clockface.bmp");
hoursHand = new Sprite("graphics/hours_hand.bmp");
minutesHand = new Sprite("graphics/minutes_hand.bmp");
secondsHand = new Sprite("graphics/seconds_hand.bmp");
// clear buffer and display image
reshape(windowWidth, windowHeight);
display();
}
void clockAnimation()
{
static time_t lastRendered = 0;
time_t unixTime = time(NULL);
struct tm *currentTime = localtime(&unixTime);
// only re-render the screen after the time interval has passed. Remember we
// do not want to render the screen to often because otherwise it will become
// too expensive.
if(lastRendered == 0 || unixTime - lastRendered >= TIME_INTERVAL)
{
// note we use negative angles because in math angles are always measured counter-clockwise
// so by using a negative angle we will get a clockwise angle needed for our clock.
hoursHand->setAngle(-1 * (30 * currentTime->tm_hour + ((int)(6 * currentTime->tm_min / 90.0)) * 7.5));
minutesHand->setAngle(-1 * 6 * currentTime->tm_min);
secondsHand->setAngle(-1 * 6 * currentTime->tm_sec);
lastRendered = unixTime;
glutPostRedisplay();
}
}
/**
* Clean up before exiting the program
*/
void cleanup()
{
delete clockFace;
delete hoursHand;
delete minutesHand;
delete secondsHand;
}
void keyboard(unsigned char key, int x, int y)
{
switch (key)
{
case ESCAPE_KEY:
cleanup();
exit(0);
break;
default:
break;
}
}
int main (int argc, char* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(windowWidth, windowHeight);
glutInitWindowPosition(100, 100);
glutCreateWindow("Analog Clock");
init();
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutIdleFunc(clockAnimation);
glutMainLoop();
return 0;
}
Download the example project here. I suggest you download the example and try building and running it. The code itself is pretty straight forward. But lets take a moment to go over. We first initalize our sprites in the init() function which will be used for the clock face and the clock hands. After that we draw the sprites on screen in the display() function. The pivot is set to be at the bottom of the clock hand were there is a hole, some simple measurements and calculations produced the numbers used there. Also note that we set the (0,0) point to be at the middle of the application window and we are using math coordinates system. This was done to simplify things so it is easier to get everything to align properly. Finally the glutIdleFunc() is used to update the clock hands every second. Since the callback clockAnimation() function gets called so often that it would be a waste to redraw the screen everytime, we only really need to do it every second. Therefore, I used a simple if statement to check if enough time elapsed between the current function call and the last time the screen was redrawn. The calculation used for the angles are pretty straight forward, I simply found the change in angle, or delta, for each of the hands and multiplied it by the time. For the hours hand since I did not want the hand jumping 30 degrees when the hour changes I created smaller increments based on the minutes. The angle 30 / 4 = 7.5 is what I need to make the hand move every 15 minutes. OpenGL and the Sprite class take care of the rest making it very elegant and easy to understand. Now it is your turn, try playing with the clock class, changing the clock face or changing the hands. After you do that you may want to try something a little more complicated, maybe a model of the solar system. To make a sprite rotate around itself as well as around another pivot point (the sun in this case) consider using the Composite pattern on the Sprite class. I may write more about this in the future, stay tuned. Downloads
- Sprite and Image Loader Class - contains the sprite and image loader classes
- Analog Clock Example - contains an eclipse project for the analog clock example. Note you can also compile and run the code using the make file, but make sure to run the executable from the AnalogClock directory.
References1. Original Sprite implementation posted in GameDev: http://www.gamedev.net/reference/articles/article2429.asp.
Add this page to your favorite Social Bookmarking websites
|
Comments
Glut is required to use this and of course g++.
I will take a closer look later on next week, as it has been a while since I wrote this.
The tagBITMAPFILEHE ADER is defined in ImageLoader.h and it is just a header used to read image files.
RSS feed for comments to this post.