Page 1 of 1

Linux Game Programming Tutorial 2: Texture Quads / 2D Images

#1 WolfCoder  Icon User is offline

  • Isn't a volcano just an angry hill?
  • member icon


Reputation: 786
  • View blog
  • Posts: 7,618
  • Joined: 05-May 05

Posted 07 September 2012 - 12:09 PM

Linux Game Programming Tutorial
2: Drawing Sprites
written by WolfCoder (2012)

Related source code and files are attached to this tutorial: Attached File  tutorial.2.tar (120K)
Number of downloads: 130

Most of you will be happy with starting out making 2D games, others would want to jump right in to 3D. How about both!? Unlike many tutorials involving SDL out there, we're going to go directly into drawing textured quads on the screen. You won't actually need to know 3D graphics quite yet to do this, but we will technically be working in 3 dimensions. Modern 2D engines are simply textured quads with shader effects here and there, the old idea of "blitting" or bitmap image transfer is obsolete. Interfaces such as DirectDraw are relics of the past, everything has been generalized into simply DirectX/OpenGL. Don't worry, OpenGL is extremly easy to use when compared to DirectX and OpenGL is the thing for Linux games.

Images

You've seen images all over the place, in various formats (.BMP, .PNG, .JPG, .TGA, .PCX, etc.). 2D games are made up of drawn bitmaps full of pixels. A rectangle array of colors, also known as pixels or "picture elements", or a picture map of bits, or a "bitmap". The goal is to load a bitmap and draw it using OpenGL (and hopefully it means with our video card). You've read files into a program before (I assume), so this isn't actually that much different. You open the file, read the data, close and there you go.. Of course then you must move it into your video card (I keep saying "hopefully", it all depends on your video card driver and implementation of OpenGL). From there, it can be used to be drawn on the screen.

/* Color */
typedef struct
{
	unsigned char blue; /* Red component */
	unsigned char green; /* Green component */
	unsigned char red; /* Blue component */
	unsigned char alpha; /* Alpha component */
}color;

/* Image */
typedef struct
{
	int width; /* Width of image */
	int height; /* Height of image */
	color *data; /* Data for image */
}image;


So that's really just it, a rectangular array of colors. You can't ask more from a bitmap. This is the data we actually care about and want to extract from any given .BMP file.

Yes, I know SDL has a SDL_LoadBMP function, but you won't really learn much if you just pull tools off the self without knowing how to make your own. Actually, if you want to load and convert more advanced formats, I recommend The SDL_Image Library. Remember how you locked surfaces to read/write pixels? That's how you would get at the data to send it to OpenGL (which we will learn later).

The Windows BMP format is very simple with the only strange quirk of the images being.. upside down. This is what happens when you let mathmaticians define file formats, you get things upside down! In computers, we go from 0 at the top left and read right and down.. Which is ironic since when we move to 3D, it's all linear algebra anyway. Let's start a new short program that can load and print out basic information about a bitmap.

Included with the tutorial is image.c and image.h, you'll notice right away something is odd about the way I load bitmaps:

/* Load image */
image *image_load_bmp(char *filename)
{
	FILE *f;
	int w,h,x,y,bypp;
	unsigned short bpp;
	char tag[2];
	image *im;
	unsigned char *data,*ptr;
	unsigned char header[IMAGE_BMP_FILE_HEADER_SIZE+IMAGE_BMP_INFO_HEADER_SIZE-2];
	color *col;
	/* Open file */
	f = fopen(filename,"rb");
	if(!f)
	{
		fprintf(stderr,"Could not open %s\n",filename);
		return 0;
	}
	/* Read tag */
	if(fread(tag,1,2,f) != 2)
	{
		fprintf(stderr,"Error reading image tag for %s\n",filename);
		fclose(f);
		return 0;
	}
	if(tag[0] != 'B' || tag[1] != 'M')
	{
		fprintf(stderr,"%s is not a valid WIN32 bitmap file\n",filename);
		fclose(f);
		return 0;
	}
	/* Read the rest of file header */
	if(fread(header,1,IMAGE_BMP_FILE_HEADER_SIZE+IMAGE_BMP_INFO_HEADER_SIZE-2,f) != IMAGE_BMP_FILE_HEADER_SIZE+IMAGE_BMP_INFO_HEADER_SIZE-2)
	{
		fprintf(stderr,"Error reading image header for %s\n",filename);
		fclose(f);
		return 0;
	}
	w = *((int*)&header[IMAGE_BMP_FILE_HEADER_SIZE-2+4]);
	h = *((int*)&header[IMAGE_BMP_FILE_HEADER_SIZE-2+4+4]);
	bpp = *((unsigned short*)&header[IMAGE_BMP_FILE_HEADER_SIZE-2+4+4+4+2]);
	bypp = bpp/8;
	/* 24 or 32 bit */
	if(bpp != 24 && bpp != 32)
	{
		fprintf(stderr,"%s is not a 24 or 32 bit bitmap (%dBPP)\n",filename,bpp);
		fclose(f);
		return 0;
	}
	/* Warning: if the image data doesn't evenly lay on a 4-byte alignment, the data may be askew */
	/* This doesn't happen in 32-bit bitmaps because each pixel is 4 bytes */
	/* You should be aligning your images to 8x8 blocks of pixels anyway */
	if(w%8 != 0 || h%8 != 0)
	{
		fprintf(stderr,"%s doesn't have width or height a multiple of 8\n",filename);
		fclose(f);
		return 0;
	}
	/* New image */
	im = image_new(w,h);
	/* Read it in */
	data = (unsigned char*)malloc(bypp*w*h);
	if(fread(data,bypp,w*h,f) != w*h)
	{
		fprintf(stderr,"Error reading image data for %s\n",filename);
		fclose(f);
		return 0;
	}
	fclose(f);
	/* Read pixels */
	for(y = 0;y < h;y++)
	{
		for(x = 0;x < w;x++)
		{
			/* Find pixels */
			ptr = &data[(x*bypp)+((h-y-1)*w*bypp)]; /* Don't ask me why WIN32 bitmaps are upside-down */
			col = &im->data[x+y*w];
			/* Copy data */
			col->red = ptr[2];
			col->green = ptr[1];
			col->blue = ptr[0];
			/* Alpha channel */
			if(bpp == 32)
				col->alpha = ptr[3];
			else
				col->alpha = 0xFF; /* Opaque */
		}
	}
	/* Done */
	free(data);
	return im;
}


I know what the more experienced of you are thinking, "why didn't you just write BITMAPFILEHEADER and BITMAPINFOHEADER structs and fread those?". Well, go do that and see what happens. Yeah that's right, those structs have special compiler directives around them disabling struct alignment somehow. It gets even worse on this 64-bit system since it pads things out to 8-byte wide boundaries! You're better off trying to avoid compiler directives, it makes porting and compiling in other systems that much harder. Besides, this isn't a function meant to be called many times a frame, so we can do with the overhead of all the I/O calls it makes (the system has to halt your program/thread until the data is read from disk, though if it's smart it had cached the entire file already so the rest of the calls return quickly).

I label all the fields in comments even if I only use a few, the format seems pretty strange because it's an artifact from the 90s. Nobody even uses 8-bit palette images anymore for space concerns, we just have so much space to spare. Anyways, this will load the bitmap, so let's write our actual program:

/*
	Image Show
	Loads and displays the given image
	written by Sayuri L. Kitsune (2012)
*/

/* Include */
#include <stdio.h>
#include "image.h"

/* Main */
int main(int argn,char **argv)
{
	image *im;
	/* Error */
	if(argn < 2)
	{
		fprintf(stderr,"No input .bmp file specified\n");
		return -1;
	}
	if(argn > 2)
	{
		fprintf(stderr,"Too many command line arguments, only specify input .bmp file\n");
		return -1;
	}
	/* Open */
	im = image_load_bmp(argv[1]);
	/* Print */
	printf("%s is %dx%d pixels\n",argv[1],im->width,im->height); /* Yeah I know, should use method access instead of direct access */
	/* Done */
	image_delete(im);
	return 0;
}


And the makefile:

test:
	make build
	./imgshow firefoxtan.bmp

build:
	-rm imgshow
	gcc -c imgshow.c image.c
	gcc imgshow.o image.o -o imgshow


The mentioned firefoxtan.bmp file is included in the download. If we run it we should get:

firefoxtan.bmp is 128x128 pixels


Now that the bitmap is loaded, we just need to draw it to the screen using OpenGL. From our previous tutorial, we already know how to get an SDL window going:

/* Open SDL window made to fit image */
SDL_Init(SDL_INIT_VIDEO);
SDL_GL_SetAttribute(SDL_GL_RED_SIZE,8); /* Configure OpenGL */
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE,8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE,32);
screen = SDL_SetVideoMode(im->width,im->height,0,SDL_HWSURFACE|SDL_OPENGL);
/* Handle */
active = 1;
while(active)
{
	/* Events */
	while(SDL_PollEvent(&ev))
	{
		if(ev.type == SDL_QUIT)
			active = 0;
	}
	/* Drawing */
}
/* Done */
SDL_Quit();


Posted Image

.. no, we don't. SDL has a built-in way of starting OpenGL for you, so this is why the code looks different now. All those calls to SDL_GL_SetAttribute are mostly to use a typical 32-bit display, you can adjust this for all the many other possible modes. Since we're using OpenGL, it's the one that needs to be told to use a double buffer this time. Don't forget the SDL_OPENGL flag in SDL_SetVideoMode, but apart from that it's just like before. You'll have to change the makefile to include the OpenGL libraries too:

test:
	make build
	./imgshow firefoxtan.bmp

build:
	-rm imgshow
	gcc -I"/usr/include/SDL" -I"/usr/include/GL" -c imgshow.c image.c
	gcc imgshow.o image.o -lSDL -lGL -o imgshow


Running the code, a blank window the size of the bitmap should appear. Guess what goes there next?

OpenGL

OpenGL is one of the most newbie friendly APIs I've seen for 3D graphics.. But then again I've only ever written OpenGL and DirectX programs, the competition isn't exactly diverse. OpenGL has more than just a color framebuffer (buffer, surface) like SDL, but for now we're going to deal with just colors. Let's clear the screen to MAGENTA and be sure that we're clearing the screen. When using 3D APIs, its a good idea to make the clear color something striking so the graphics are easier to debug.

/* Open SDL window made to fit image */
SDL_Init(SDL_INIT_VIDEO);
SDL_GL_SetAttribute(SDL_GL_RED_SIZE,8); /* Configure OpenGL */
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE,8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE,32);
screen = SDL_SetVideoMode(im->width,im->height,0,SDL_HWSURFACE|SDL_OPENGL);
/* Set up opengl */
glClearColor(1.0f,0.0f,1.0f,1.0f); /* Magenta, note the colors are now 0.0~1.0 floating point. */
/* Handle */
active = 1;
while(active)
{
	/* Events */
	while(SDL_PollEvent(&ev))
	{
		if(ev.type == SDL_QUIT)
			active = 0;
	}
	/* Erase old picture */
	glClear(GL_COLOR_BUFFER_BIT); /* Clear the color buffer */
	/* Flush out the commands (possibly pointless for a single-machine setup, but you never know) */
	glFlush();
	/* And flip the buffers, note this is different for OpenGL! */
	SDL_GL_SwapBuffers();
}
/* Done */
SDL_Quit();
image_delete(im);
return 0;


Posted Image

You call clear and the buffer gets wiped out to whatever clear color you set. You might not need glFlush there, but it's just to be safe. The one important thing to remember is that the buffer flipping function to display the resulting picture is now SDL_GL_SwapBuffers, different than normal SDL. Now, let's draw some lines. Let's work in 2D for now. How does OpenGL work in 2D? You'll find OpenGL is very flexible and can work in whatever coordinate mode you feel like! I'm not going to load your mind with the MODEL/WORLD/PROJECTION matrix system yet, but we need to tell OpenGL to change these. Don't worry if you have no idea what I'm talking about, you'll understand when we actually use them. Consider the following snippet:

/* Prepare to draw a 2D scene */
glMatrixMode(GL_PROJECTION); /* This is how your stuff gets transformed for the screen */
glLoadIdentity(); /* Clear matrix */
glOrtho(0,im->width,im->height,0,-1,1); /* Left,Right BOTTOM, and then top, and then near and further */
glMatrixMode(GL_MODELVIEW);
glLoadIdentity(); /* Clear matrix. Note this means a point here just gets directly put on screen */


Do you know your linear algebra? No? You'll need a refresher when you move on to the 3D stuff, but for now this setting takes any point in 3D space and directly takes the X and Y positions and puts them on screen. I've mapped the points to the actual pixels and we don't need the Z position. X and Y are the horizontal and vertical positions on your screen as you may be used to. However, the Z axis is pointing directly out of the screen and at your face.

Let's put an ugly green line on the screen:

/* Draw some lines */
glColor3f(0.0f,1.0f,0.0f);
glBegin(GL_LINES);
glVertex3f(8,8,0);
glVertex3f(32,8,0);
glEnd();


Posted Image

And there you go. I'm not going to do them in this tutorial, but I recommend you play around with more than just GL_LINES and see what shapes work. However, we're here to make graphics further beyond the original StarFox game, we need pictures.

Texture / Image

Loading our image into OpenGL as a texture is a little involved. Let's step carefully through the process. For 2D games, we just want unfiltered, 32-bit pictures. OpenGL refers to textures with just numbers. You can use any number you want, but there's a function that lets you ask OpenGL for numbers not currently in use. I'm worried at the amount of quick guides and tutorials that don't do this. Anyways, it's just glGenTextures. Doing this doesn't actually load the texture yet. You need to select the texture so that the next series of OpenGL commands modify it using glBindTexture. It makes your life easier to create a texture variable, something like this:

/* Texture */
typedef struct
{
	int width,height; /* We still want to know the size of texture */
	GLuint tex; /* Integer referring to texture */
}texture;


Then through the following series of commands, we specify the texture to be unfiltered and that the data is made up of bytes:

/* Configure texture */
glPixelStorei(GL_UNPACK_ALIGNMENT,1);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);


GL_REPEAT will make sense later when you specify texture coordinates for each point, but it means if you set them outside the texture box that the texture will simply repeat. GL_NEAREST is the part that actually makes the texture unfiltered and get that pixel look if you draw them scaled up or rotated. If for some reason you don't want that, you can put other settings in these two calls. The difference between "MAG" and "MIN" mean when you shrink the image for "MIN" or stretch it out for "MAG". This only set the settings, next we must load the data:

/* Load pixel data */
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,tex->width,tex->height,0,GL_BGRA,GL_UNSIGNED_BYTE,(unsigned char*)im->data);


There are many different settings here. I recommend you look up the documentation for glTexImage2D, but for now this is what the call looks like to load image data in the format our sample program does and make a generic 32-bit texture out of it. You don't have to understand the settings here yet as long as you know there's tons of combinations you could do. This is it, usually the picture should now exist in your video card and you can even delete the original image from normal memory if you want. Deleting the OpenGL texture is as easy as glDeleteTextures.

/* Just delete */
glDeleteTextures(1,&tex->tex);

http://www.opengl.or...eteTextures.xml
Before we go ahead and draw firefoxtan.bmp like we were going to, let's draw a smaller sprite to make sure we can move it around and control it.

/* Load test */
sim = image_load_bmp("sayuri.bmp");
stex = imgshow_new_texture(sim);
image_delete(sim);


This texture is much smaller and we can fit it inside the window. Drawing the texture is just like drawing regular things, except we add calls to add texture verticies. Imagine if the square picture had coordinates (0,0) on the top left, (1,0) on the top right, (1,1) on the bottom right, and (0,1) for bottom left. It's always 0~1 no matter the actual resolution of the texture, which doesn't make that much sense for our 2D games but much more sense for 3D texturing purposes. Let's write a function that draws a textured 2D quad wherever we want- you know the purpose of this tutorial!

/* Draw a texture simply */
void imgshow_draw_texture(texture *tex,int x,int y)
{
	float w2,h2;
	/* Measure the half size of the texture (why are we doing this?) */
	w2 = (float)(tex->width/2);
	h2 = (float)(tex->height/2);
	/* Select the texture */
	glBindTexture(GL_TEXTURE_2D,tex->tex);
	/* Select.. a white color!? */
	glColor3f(1.0f,1.0f,1.0f);
	/* Draw the quad */
	glBegin(GL_QUADS);
	glTexCoord2f(0.0f,0.0f); /* Top left */
	glVertex3f(-w2,-h2,0.0f);
	glTexCoord2f(1.0f,0.0f); /* Top right */
	glVertex3f(w2,-h2,0.0f);
	glTexCoord2f(1.0f,1.0f); /* Bottom right */
	glVertex3f(w2,h2,0.0f);
	glTexCoord2f(0.0f,1.0f); /* Bottom left */
	glVertex3f(-w2,h2,0.0f);
	glEnd();
}


And then:

/* Draw */
imgshow_draw_texture(stex,0,0);


Posted Image

..but then, it's just a white square half off of the screen at the top-left! Yes, you could directly add the positions yourself to make it match, or you could let OpenGL do all the hard work:

/* Transform */
glPushMatrix();
glTranslatef(w2,h2,0.0f); /* Convert from center anchor to top-left anchor */
glTranslatef((float)x,(float)y,0.0f); /* Move into user-defined position */
/* Select.. a white color!? */
glColor3f(1.0f,1.0f,1.0f);
/* Draw the quad */
glBegin(GL_QUADS);
glTexCoord2f(0.0f,0.0f); /* Top left */
glVertex3f(-w2,-h2,0.0f);
glTexCoord2f(1.0f,0.0f); /* Top right */
glVertex3f(w2,-h2,0.0f);
glTexCoord2f(1.0f,1.0f); /* Bottom right */
glVertex3f(w2,h2,0.0f);
glTexCoord2f(0.0f,1.0f); /* Bottom left */
glVertex3f(-w2,h2,0.0f);
glEnd();
/* Return to old matrix */
glPopMatrix();


You'll notice now there's glPushMatrix and glPopMatrix. This lets us save whatever mode was before and modify things. Think of a given sprite as its own thing, like a model. This helps you localize all these transformations to each sprite. If you didn't use the matrix stack, each drawing function would mess with the next ones since the matrix would be stuck in whatever mode we had before for the model view matrix (notice how we don't set the matrix mode since we should only be working in model mode). Too confusing? It will actually make more sense and become more important when you move to 3D. For 2D games, a draw textured quad function is most of what you need. This code still draws a white square, we actually have to turn on textures with:

glEnable(GL_TEXTURE_2D);


Posted Image

And now the little sprite is drawn! We can move it about too:

imgshow_draw_texture(stex,16,32);


Posted Image

Since they're all at the same z-value, the pictures appear in the order you draw them. Just to show you, let's draw the original picture first:

/* Load as texture */
tex = imgshow_new_texture(im);


imgshow_draw_texture(tex,0,0);


Posted Image

Ah! AAAH! You realize the magenta background from the sprite is still there! You didn't notice it before because the clear color was also magenta. You're going to have to "color key" the image. Thanks to our OpenGL texture format, it's as simple as going to every magenta pixel in the original image and replacing it with alpha value 0 before we pass it off to OpenGL:

/* Key a image */
void imgshow_color_key(image *im)
{
	int x,y;
	color col;
	for(y = 0;y < im->height;y++)
	{
		for(x = 0;x < im->width;x++)
		{
			col = im->data[x+y*im->width];
			if(col.red == 255 && col.green == 0 && col.blue == 255)
				col.alpha = 0;
			im->data[x+y*im->width] = col;
		}
	}
}


Apply it to:

/* Load test */
sim = image_load_bmp("sayuri.bmp");
imgshow_color_key(sim);
stex = imgshow_new_texture(sim);
image_delete(sim);


One final important thing is that we need to enable "alpha blending". Without it, this code does nothing when the texture is drawn. For the most basic blending that does what you expect, replace the single glEnable call with this:

glEnable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);


Posted Image

And now the background is gone! Finally!

Rotation, Scaling, and Fading

These three effects are quite non-trivial in pure SDL. You'll begin to realize why I started right away with OpenGL. For rotation and scaling, we can just stick the matrix functions at the end of the previous transformation. Now for the important note, the transformations work from last call to first:

/* Draw a texture simply */
void imgshow_draw_texture(texture *tex,int x,int y,float sx,float sy,float r,float a)
{
	float w2,h2;
	/* Measure the half size of the texture (why are we doing this?) */
	w2 = (float)(tex->width/2);
	h2 = (float)(tex->height/2);
	/* Select the texture */
	glBindTexture(GL_TEXTURE_2D,tex->tex);
	/* Transform */
	glPushMatrix();
	glTranslatef(w2,h2,0.0f); /* Convert from center anchor to top-left anchor */
	glTranslatef((float)x,(float)y,0.0f); /* Move into user-defined position */
	glRotatef(r,0.0f,0.0f,1.0f); /* Rotate the image */
	glScalef(sx,sy,1.0f); /* Scale the image */
	/* Select.. a white color!? */
	glColor4f(1.0f,1.0f,1.0f,a);
	/* Draw the quad */
	glBegin(GL_QUADS);
	glTexCoord2f(0.0f,0.0f); /* Top left */
	glVertex3f(-w2,-h2,0.0f);
	glTexCoord2f(1.0f,0.0f); /* Top right */
	glVertex3f(w2,-h2,0.0f);
	glTexCoord2f(1.0f,1.0f); /* Bottom right */
	glVertex3f(w2,h2,0.0f);
	glTexCoord2f(0.0f,1.0f); /* Bottom left */
	glVertex3f(-w2,h2,0.0f);
	glEnd();
	/* Return to old matrix */
	glPopMatrix();
}


Not "simply" anymore. OpenGL actually uses degrees for rotation, which is nice. Notice how the image gets multiplied with whatever the current color is, so if you keep it white but adjust the alpha, you can also make the image fade in and out. Neat, take a look at it with:

imgshow_draw_texture(stex,16,32,2.0f,2.0f,45.0f,0.7f);


Posted Image

It makes the sprite twice as big, rotated a bit clockwise and partially faded out. Like I said, last call to first. The image is scaled and rotated first before translating. If we did it the other way around, the image would go flying off since its center is not longer.. actually its center! Go ahead and mess with the code included in this tutorial and see what you can get it to do. Next time, we're going to get a character to walk around the screen using user input. This would combine the objects and time from the first tutorial, the sprites from this tutorial, and add input.

If you need to, you can download all the source code and Makefiles involved in this tutorial here: Attached File  tutorial.2.tar (120K)
Number of downloads: 130

Is This A Good Question/Topic? 1
  • +

Replies To: Linux Game Programming Tutorial 2: Texture Quads / 2D Images

#2 stayscrisp  Icon User is offline

  • フカユ
  • member icon

Reputation: 1009
  • View blog
  • Posts: 4,209
  • Joined: 14-February 08

Posted 18 October 2012 - 05:41 AM

Nice tutorial!
Was This Post Helpful? 0
  • +
  • -

#3 Duality4y  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 06-April 13

Posted 06 April 2013 - 03:02 PM

this is a great tutorial!! Making a lot of progress! thanks a lot :)!!
just want to download the .tar so i could use your tutorial image.h and image.c
says:
[#10173] We could not find the attachment you were attempting to view.

Greetings,
Duality
Was This Post Helpful? 0
  • +
  • -

#4 sthibault  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 23-September 13

Posted 23 September 2013 - 11:09 AM

The links to download the code are broken, and not all of the constants in the image stuff are defined anywhere. Can you fix those links?
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1