Jump to content
  • Advertisement
Sign in to follow this  
Ranger_One

OpenGL So, you want to make a 2d OpenGL game engine (long)

This topic is 4574 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

A few months ago I posted up a thread on loading models from the wavefront OBJ file format. I enjoyed doing so, and the thread got a lot of response. There are a lot of tutorials out there that cover 2d OpenGL. However, they all lack a level of completeness and style in my opinion. I feel the need to lay down another, with the following ideals: - An interface not unlike OpenGL's design, though supporting threading. - Using DevIL for image processing. - Support for 2d texture-mapped fonts based those created by LMNOpc's BitmapFontBuilder - Free of underlying wrapper code (Win, Lin, SDL, or GLFW for instance) Those first three are obvious and easy to acheive, the last though is the kicker. I'll be supplying functions that need 'filled in' for whatever wrapper is used. I need access to mutex for instance, and that is something that needs filled in by the user. First, let me prototype the functions I'm going to explain in the first post.
// ********************************************************************************
// Config
typedef struct _v2dConfig {
     int ScreenWidth, ScreenHeight;
} v2dConfig;
extern v2dConfig v2d;

// ********************************************************************************
// General Functions
void v2dEnter();
void v2dExit();
void v2dFlip();

// ********************************************************************************
// Image Functions
int v2dLoadImage(char *Filename);
void v2dFreeImage(int i);
void v2dRealizeImage(int v); // usually called internally
void v2dBindImage(int v);
void v2dImage(float x, float y,int i);




v2dEnter() initializes the 2d projection mode, readying OpenGL for drawing in 2d. v2dExit() closes up 2d drawing mode. vxdFlip() has to be filled in by the user, and flips the OpenGL buffers to show what has been done. v2dLoadImage() reads an image file from storage using DevIL and loads it into memory. It returns a unique number to identify the texture/image. v2dFreeImage() releases all the memory attached to an image object when it is no longer needed. v2dRealizeImage() pumps the loaded image into OpenGL, and is called internally as needed. I separate this from ~LoadImage() such that you may call that function from another thread. The ~RealizeImage() is always called in the main context holding thread and can use valid OpenGL calls. v2dBindImage() binds the current image for OpenGL drawing and makes sure it has been realized with ~RealizeImage(). v2dImage() draws the image i at the specified 2d coords on screen.

void v2dEnter()
{
    glMatrixMode(GL_PROJECTION); // select the projection matrix
    glLoadIdentity(); // clear it
    glOrtho(0,v2d.ScreenWidth,v2d.ScreenHeight,0,-1,1); // setup a new projection for 2d
    glMatrixMode(GL_MODELVIEW); // select the model matrix
    glLoadIdentity(); // clear it

    glDisable(GL_DEPTH_TEST); // we aren't going to depth test
    glDisable(GL_LIGHTING); // or light the scene
    glEnable(GL_BLEND); // we however do use alpha and blending
    glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); // for transparency
    glColor4f(1.0f,1.0f,1.0f,1.0f); // make sure our normal color is full white
}

void v2dExit()
{
    // If we need to cleanup custom information, here is where we would
    // right now we don't need to though... (stub)
}

void vxdFlip()
{
    // call glfwSwapBuffers(), SwapBuffers(HDC hdc), SDL_GL_SwapBuffers(), etc.
    // make sure the buffers are swapped and we see what we draw...
}




[Edited by - Ranger_One on January 11, 2006 10:30:59 AM]

Share this post


Link to post
Share on other sites
Advertisement
Now I can move onto the image functions, and after which I'll talk a bit about how and why things are handled the way they are.

First, we need to define a struct to hold the information for each image/texture object.

// we need a struct to hold the information
typedef struct _v2dImg {
GLuint TexId; // the OpenGL texture name for this image
ILuint ImageId; // the DevIL image name for this image
ILinfo Info; // info about the image pulled from DevIL
char SourceFile[256]; // the source file for the image

float x,y,w,h; // rendering coords, in case we had to enlarge the canvas...
} v2dImg;

vector<v2dImg*> v2dImageStack;
vector<int> v2dFreeImage;




- The last part is important. I'm talking about the rendering coords. The load function allows the loading of images that are any dimension, but textures in OpenGL have to use dimensions that are a power of two. The function enlarges the canvas of the image to make it fit those restrictions, and the rendering coords allow the engine to draw the original image in spite of the change.
- The vector<> defines create two stacks for images. The first holds actual image data, and the second references 'free' entries in the stack. Since we are going for speed in an engine, I never actually free any allocated v2dImg. Instead I reuse objects by marking them as free.

Now lets get to the v2dLoadImage function...


int v2dLoadImage(char *Filename)
{
int ret = -1; // our returned value
// first, let DevIL try and load the image
ILuint DevilImageName;
ILinfo DevilImageInfo;

ilGenImages(1,&DevilImageName); // new devil image name
ilBindImage(Image[v]->ImageId); // now bound into the context
if (ilLoadImage((char*)Filename) == IL_TRUE) // did it load?
{
iluGetImageInfo(&DevilImageInfo); // read back the info
ilConvertImage(IL_RGBA,IL_BYTE); // make sure we are a 32-bit texture now...
int diWidth = DevilImageInfo.Width;
int diHeight = DevilImageInfo.Hieght; // get dims from info

diWidth = 1 << (int)floor((log((double)diWidth)/log(2.0f)) + 0.5f);
diHeight = 1 << (int)floor((log((double)diHeight)/log(2.0f)) + 0.5f);
// ok, the dims have been set to a next larger power of 2
// we should check for exceeding max texture size, but I don't :P

if (v2dFreeImage.size())
{
// we have free images, use one
ret = v2dFreeImage.back(); // get the free image number
v2dFreeImage.pop_back(); // its used so remove it from the list
} else
{
// allocate a new image object on the stack
v2dImg *ni = new v2dImg;
ret = v2dImageStack.size();
v2dImageStack.push_back(ni);
}

v2dImageStack[ret]->TexId = 0xFFFFFFFF; // not yet valid
v2dImageStack[ret]->ImageId = DevilImageName; // our image name
v2dImageStack[ret]->Info = DevilImageInfo; // our image info
strcpy(v2dImageStack[ret]->SourceName,Filename); // our filename (no length check? :P)

// here we calculate the rendering coords
v2dImageStack[ret]->w = (float)DevilImageInfo.Width / (float)diWidth;
v2dImageStack[ret]->h = (float)DevilImageInfo.Height / (float)diHeight;
v2dImageStack[ret]->x = 0.0f;
v2dImageStack[ret]->y = 1.0f - v2dImageStack[fin]->h;
}
return ret;
}




As you can see, the function either returns a new image name or the invalid -1. The code that generates the proper power of 2 dimension was pulled from the nehe IPicture base code.

[Edited by - Ranger_One on January 11, 2006 11:03:31 AM]

Share this post


Link to post
Share on other sites
The other image functions are simpler. Here they are in some detail. You should note that so far I have left a lot of source out, like full includes and such. I'll be linking to a download from everything soon that will provide full files for the project :)

v2dFreeImage() just clears the image data (if it has any) and then marks that image name as free, adding it to the free name stack.

void v2dFreeImage(int ImageName)
{
// since we don't actually remove any image objects
// this is an simple as making the image invalid and
// putting it's number on the free image stack/vector
if (v2dImageStack[ImageName]->TexId != 0xFFFFFFFF)
{
glDeleteTextures(1,&v2dImageStack[ImageName]->TexId);
v2dImageStack[ImageName]->TexId = 0xFFFFFFFF;
}
if (v2dImageStack[ImageName]->ImageId != -1)
{
ilDeleteImages(1,&v2dImageStack[ImageName]->ImageId);
v2dImageStack[ImageName]->ImageId = -1;
}
v2dImageStack[ImageName]->SourceFile[0] = 0;
v2dFreeImage.push_back(ImageName);
}



v2dBindImage() intelligently handles images that aren't yet bound into the OpenGL context, something purposely left out of v2dLoadImage().

// this either binds the texture in gl, or realizes the
// imsge into a texture if needed
void v2dBindImage(int ImageName)
{
if (v2dImageStack[ImageName]->TexId == 0xFFFFFFFF)
RealizeImage(ImageName);
else
glBindTexture(GL_TEXTURE_2D,v2dImageStack[ImageName]->TexId);
}



v2dRealizeImage() moves image data from DevIL into OpenGL. I don't use ilut, so neither does thi function (though that would have been easier).

void v2dRealizeImage(int ImageName)
{
// bind the image if valid, or scram back out if not
if (v2dImageStack[ImageName]->ImageId == -1) return;
else ilBindImage(v2dImageStack[ImageName]->ImageId);

// prepare OpenGL to accept the image data
glGenTextures(1,&v2dImageStack[ImageName]->TexId);
glBindTexture(GL_TEXTURE_2D, v2dImageStack[ImageName]->TexId);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); // Linear Filtering
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); // Linear Filtering
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ilGetInteger(IL_IMAGE_WIDTH),
ilGetInteger(IL_IMAGE_HEIGHT), 0, ilGetInteger(IL_IMAGE_FORMAT), GL_UNSIGNED_BYTE,
ilGetData()); /* Texture specification - ilGetInteger(IL_IMAGE_BPP) */
}

Share this post


Link to post
Share on other sites
finally, I get to the last function!

v2dImage() renders the loaded image onto the screen at coords specified by x and y.


void v2dImage(float x, float y, int ImageName)
{
v2dBindImage(ImageName); // make sure we have the image bound

v2dImg *pi = v2dImageStack[ImageName]; // make things clearer below

glBegin(GL_QUADS); // using quads makes this easy

glTexCoord2f(pi->x,pi->y); // upper left corner tex coord
glVertex2f(x,y); // upper left corner screen coord
glTexCoord2f(pi->x+pi->w,pi->y); // upper right tex
glVertex2f(x+pi->Info.Width,y); // upper right screen
glTexCoord2f(pi->x+pi->w,pi->y+pi->h); // lower right tex
glVertex2f(x+pi->Info.Width,y+pi->Info.Height); // lower right screen
glTexCoord2f(pi->x,pi->y+pi->h); // lower left tex
glVertex2f(x,y+pi->Info.Height); // lower right screen

glEnd(); // all done!
}

Share this post


Link to post
Share on other sites
Now that I have covered all the described functions, let me fill in some missing information. First, I actually forgot some functions. There is no v2dClear(), no v2d Color(), and so on. My next post will fix that minor failing.

Second, the entire interface used is rather abstract. In fact it is abstract enough to allow an entirely different renderer, like say, directx. That wasn't my intent but merely a side effect of the style I employed.

Third, I don't mean to make you think I was programming C. The code is C++ only and uses the STL vector class. The interface is meant to mirror GL, and therefore looks much like C (since OpenGL is C only). Make no C assumptions.

Fourth, I need to lay down the code that supports frames. Frames are segments of an image. Say you have a sprite image that contains 16 (4x4, or 8x2 etc) subimages. Each subimage is a frame that references the main image. Say Frame, think Sprite. :)

Any questions or comments can be posted here, PM'd to me, or sent to dev@inkarbon.com

Share this post


Link to post
Share on other sites
Easy on the GPU there buddy! If I was to have a particle system, or even a bunch of tiles, that system would hurt performence. And drawing quads, I understand VBO's might be too much for newbies, but what about a display list for the quad, and then a glScalef?

Otherwise - nice work, I like the api you give in it - abstracting things in tutorials can really end up confusing people, and you've avoiding that but kept the system organized. Good job :)

Share this post


Link to post
Share on other sites
Here is the missing clear function and some color support :)


// the missing function...
void v2dClear();

// **************************************************************
// Color Functions
int v2dColor(float r, float g, float b, float a); // all rgba values
int v2dColor(float r, float g, float b); // just rgb
int v2dColor(char *hex); // from a Hex string
int v2dColor(int ColorName); // 16 basic colors
void v2dColorSet(int ColorName, float r, float g, float b, float a); // redefine a color
void v2dColorGet(int ColorName, float *r, float *g, float *b, float *a); // read a color's components
void v2dFreeColor(int ColorName);
void v2dCast(int CastColor); // casts a color (blend) across all color calls
void v2dPick(int ColorName); // selects a color





The implementation for v2dClear couldn't be easier...

void v2dClear()
{
glClear(GL_COLOR_BUFFER_BIT);
}





My color support code uses the same style of storage and the image names. It never actually releases any memory but uses it instead. Really shouldn't be a problem here anyway, since each color is only 4 floats = 16 bytes.


#define V2D_COLOR_RED 0
#define V2D_COLOR_GREEN 1
#define V2D_COLOR_BLUE 2
#define V2D_COLOR_ALPHA 3

// the sixteen named colors (standard)
#define V2D_BASIC_BLACK 0
#define V2D_BASIC_MAROON 1
#define V2D_BASIC_GREEN 2
#define V2D_BASIC_TEAL 3
#define V2D_BASIC_NAVY 4
#define V2D_BASIC_PURPLE 5
#define V2D_BASIC_OLIVE 6
#define V2D_BASIC_GRAY 7
#define V2D_BASIC_SILVER 8
#define V2D_BASIC_BLUE 9
#define V2D_BASIC_LIME 10
#define V2D_BASIC_AQUA 11
#define V2D_BASIC_RED 12
#define V2D_BASIC_FUCHSIA 13
#define V2D_BASIC_YELLOW 14
#define V2D_BASIC_WHITE 15

typedef struct _v2dColorType {
float Index[4];
} v2dColorType; // could have used a union here I guess, but whatever...
v2dColorType v2dCastColor = { 1.0f, 1.0f, 1.0f, 1.0f };
vector<v2dColorType> ColorStack;
vector<int> FreeColor;

float v2dColorBasic[] = {
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.5f, 0.0f, 1.0f,
0.0f, 0.5f, 0.5f, 1.0f,
0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.0f, 0.5f, 1.0f,
0.5f, 0.5f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f,
0.75f, 0.75f, 0.75f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f
};

int v2dColor(float r, float g, float b, float a)
{
int NewColorName;
v2dColorType C;

C.Index[V2D_COLOR_RED] = r;
C.Index[V2D_COLOR_GREEN] = g;
C.Index[V2D_COLOR_BLUE] = b;
C.Index[V2D_COLOR_ALPHA] = a;

if (FreeColor.size())
{
NewColorName = FreeColor.back();
FreeColor.pop_back();
ColorStack[NewColorName] = C;
} else
{
NewColorName = ColorStack.size();
ColorStack.push_back(C);
}

return NewColorName;
}

int v2dColor(float r, float g, float b)
{
int NewColorName;
v2dColorType C;

C.Index[V2D_COLOR_RED] = r;
C.Index[V2D_COLOR_GREEN] = g;
C.Index[V2D_COLOR_BLUE] = b;
C.Index[V2D_COLOR_ALPHA] = 1.0f;

if (FreeColor.size())
{
NewColorName = FreeColor.back();
FreeColor.pop_back();
ColorStack[NewColorName] = C;
} else
{
NewColorName = ColorStack.size();
ColorStack.push_back(C);
}

return NewColorName;
}

int v2dColor(char *hex)
{
int NewColorName;
v2dColorType C;
unsigned int R, G, B, A;
R = B = G = A = 0xFF;
sscanf(hex,"%2x%2x%2x%2x",&R,&G,&B,&A);

C.Index[V2D_COLOR_RED] = (float)R / 255.0f;
C.Index[V2D_COLOR_GREEN] = (float)G / 255.0f;
C.Index[V2D_COLOR_BLUE] = (float)B / 255.0f;
C.Index[V2D_COLOR_ALPHA] = (float)A / 255.0f;

if (FreeColor.size())
{
NewColorName = FreeColor.back();
FreeColor.pop_back();
ColorStack[NewColorName] = C;
} else
{
NewColorName = ColorStack.size();
ColorStack.push_back(C);
}

return NewColorName;
}

int v2dColor(int ColorId)
{
int NewColorName;
v2dColorType C;

C.Index[V2D_COLOR_RED] = v2dColorBasic[ColorId*4];
C.Index[V2D_COLOR_GREEN] = v2dColorBasic[ColorId*4+1];
C.Index[V2D_COLOR_BLUE] = v2dColorBasic[ColorId*4+2];
C.Index[V2D_COLOR_ALPHA] = v2dColorBasic[ColorId*4+3];

if (FreeColor.size())
{
NewColorName = FreeColor.back();
FreeColor.pop_back();
ColorStack[NewColorName] = C;
} else
{
NewColorName = ColorStack.size();
ColorStack.push_back(C);
}

return NewColorName;
}

void v2dColorSet(int ColorName, float r, float g, float b, float a)
{
ColorStack[ColorName].Index[V2D_COLOR_RED] = r;
ColorStack[ColorName].Index[V2D_COLOR_GREEN] = g;
ColorStack[ColorName].Index[V2D_COLOR_BLUE] = b;
ColorStack[ColorName].Index[V2D_COLOR_ALPHA] = a;
}

void v2dColorGet(int ColorName, float *r, float *g, float *b, float *a)
{
if (r) *r = ColorStack[ColorName].Index[V2D_COLOR_RED];
if (g) *g = ColorStack[ColorName].Index[V2D_COLOR_GREEN];
if (b) *b = ColorStack[ColorName].Index[V2D_COLOR_BLUE];
if (a) *a = ColorStack[ColorName].Index[V2D_COLOR_ALPHA];
}


void v2dFreeColor(int ColorName)
{
FreeColor.push_back(ColorName);
}

void v2dCast(int CastColor)
{
v2dCastColor = ColorStack[CastColor];
}

void v2dPick(int ColorName)
{
v2dColorType SelColor = ColorStack[ColorName];

glColor4f(SelColor.Index[V2D_COLOR_RED] * v2dCastColor.Index[V2D_COLOR_RED,
SelColor.Index[V2D_COLOR_GREEN] * v2dCastColor.Index[V2D_COLOR_GREEN,
SelColor.Index[V2D_COLOR_BLUE] * v2dCastColor.Index[V2D_COLOR_BLUE,
SelColor.Index[V2D_COLOR_ALPHA] * v2dCastColor.Index[V2D_COLOR_ALPHA);
}





The color code allows the whole screen to be blended with another color. This is set by calling v2dCast() with a color name. And yes, the v2dColorTypes internal structure isn't beautiful, but it is easy to read.

[Edited by - Ranger_One on January 11, 2006 11:27:42 AM]

Share this post


Link to post
Share on other sites
Acid2,

Thanks for the praise :) As far as more advanced implementations are concerned, I was going to introduce optimizations as I go. I picture this as part I of a several part tutorial. Though it should work fine for something like a blended HUD or such as is...

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!