views:

2090

answers:

7

Hi,

I want to make a 2D tiled background system on the iPhone. Something that takes a tilemap and tileset image(s) and converts it into the full map on the screen.

Just doing some messing around, my first approach was to create a polygon for each tile. This worked fine until I started testing it for 400 polygons or so, then it started running very slowly. I'm just wondering - is this method of several polygons just not the way to go? Or am I doing something wrong with it? I'll post code later if needed but my main question is "Would 400 small polygons run slowly on the iPhone or am I just doing something wrong?"

I also considered another way which was to, during initialization, create the map texture by code out of the tilemap/tilesets, and then stick that on ONE large polygon. So yeah...any feedback on how I should go about something like this?

I know someone will mention this - I gave consideration to trying cocos2d, but I've got my reasons for not going that route.

Thanks, Scott

A: 

Using the glDrawTex extension may also be a possibility.

John Calsbeek
A: 

Stanford iTune University has a podcast on Optimizing OpenGL for iPhone.

But the basic idea are these:

  1. Batch Geometry, combining various vertex array into a single big vertice array. This should reduce x number of gl*Pointer calls into a single gl*Pointer call.

  2. Texture Atlases, using a single texture for all the different tiles, differences being the regions to use for each tile. Just bind once to the texture for all tile drawing.

  3. Interleaved Arrays, combining various parts of a point (eg. vertex, texture coordinates, color) into a single array. This should reduce gl*Pointer calls to a single call.

  4. Indexed triangles, allowing you to reuse geometry information

  5. Using Short instead of Float if possible for geometery information, as it is smaller.

That's just a general opengl optimization guidelines. As for tile engine, well..

  1. Do your own culling before sending the data to opengl. What you don't draw, you save.

I think that's what I can think of so far.

Kent Lai
+1  A: 

Your problem is almost certainly that you're binding textures 400 times, and not anything else. You should have all your tiles in one big texture atlas / sprite sheet and instead of rebinding your textures you should just bind your atlas once and then draw small parts of it. If you do this, you should be able to draw thousands of tiles with no real slowdown.

You can draw your sprite like this:

//Push the matrix so we can keep it as it was previously.
glPushMatrix();

//Store the coordinates/dimensions from a rectangle.
float x = CGRectGetMinX(rect);
float y = CGRectGetMinY(rect);
float w = CGRectGetWidth(rect);
float h = CGRectGetHeight(rect);

float xOffset = x;
float yOffset = y;

if (rotation != 0.0f)
{
 //Translate the OpenGL context to the center of the sprite for rotation.
 glTranslatef(x+w/2, y+h/2, 0.0f);

 //Apply the rotation over the Z axis.
 glRotatef(rotation, 0.0f, 0.0f, 1.0f);

 //Have an offset for the top left corner.
 xOffset = -w/2;
 yOffset = -h/2;
}

// Set up an array of values to use as the sprite vertices.
GLfloat vertices[] =
{
 xOffset, yOffset,
 xOffset, yOffset+h,
 xOffset+w, yOffset+h,
 xOffset+w, yOffset,
};

// Set up an array of values for the texture coordinates.
GLfloat texcoords[] =
{
 CGRectGetMinX(clippingRect), CGRectGetMinY(clippingRect),
 CGRectGetMinX(clippingRect), CGRectGetHeight(clippingRect),
 CGRectGetWidth(clippingRect), CGRectGetHeight(clippingRect),
 CGRectGetWidth(clippingRect), CGRectGetMinY(clippingRect),
};

//If the image is flipped, flip the texture coordinates.
if (flipped)
{
 texcoords[0] = CGRectGetWidth(clippingRect);
 texcoords[2] = CGRectGetWidth(clippingRect);
 texcoords[4] = CGRectGetMinX(clippingRect);
 texcoords[6] = CGRectGetMinX(clippingRect);
}

//Render the vertices by pointing to the arrays.
glVertexPointer(2, GL_FLOAT, 0, vertices);
glTexCoordPointer(2, GL_FLOAT, 0, texcoords);

// Set the texture parameters to use a linear filter when minifying.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

//Allow transparency and blending.
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

//Enable 2D textures.
glEnable(GL_TEXTURE_2D);

//Bind this texture.
if ([Globals getLastTextureBound] != texture)
{
 glBindTexture(GL_TEXTURE_2D, texture);
}

//Finally draw the arrays.
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

//Restore the model view matrix to prevent contamination.
glPopMatrix();

The two CGRect's I used are just for ease's sake. You can specify the X, Y, width, and height to draw the image, and you can specify where in the image you want to draw using the clippingRect. With the clipping rect, (0, 0, 1, 1) is the entire image, whereas (0, 0, 0.25, 0.25) would only draw the top left corner. By changing the clipping rect, you can put all sorts of different tiles in the same texture, then you only need to bind once. Way cheaper.

Eli
A: 

Thanks for the advice, but it is still slow. It's actually not even when I have 400 anymore, it even happens with only a couple. I'm obviously doing something wrong. I'm just going to post my code...I'm a newb at OpenGL ES. It's fairly simple code, so could someone please locate the nature of the slowdown? I'd be very appreciative.

This is code that runs every single frame in the loop:

    const GLubyte squareColors[] = {
 255, 0, 0, 0,
 255, 255, 255, 0, 
 0, 0, 0, 0, //0,     100,   0,   0,
 0, 0, 0, 0, //100,   0, 140, 255,
}; 

const GLfloat squareTexCoords[] = {
    0.0, 0.03125,
    0.03125, 0.03125,
    0.0, 0.0,
    0.03125, 0.0
};

[EAGLContext setCurrentContext:context];

glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
glViewport(0, 0, backingWidth, backingHeight);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrthof(-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);
glMatrixMode(GL_MODELVIEW);
glRotatef(3.0f, 0.0f, 0.0f, 1.0f);

glClearColor(0.9f, 0.9f, 0.9f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

glEnableClientState(GL_VERTEX_ARRAY);
glColorPointer(4, GL_UNSIGNED_BYTE, 0, squareColors);
glTexCoordPointer(2, GL_FLOAT, 0, squareTexCoords);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);

glEnable(GL_TEXTURE_2D);

glBindTexture(GL_TEXTURE_2D, testName);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 1.0f);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

//create tiles
glVertexPointer(2, GL_FLOAT, 0, tilevertices);

for (int drawindex = 0; drawindex < vsize * vsize * 4; drawindex += 4)
{
 //draw tile
 glDrawArrays(GL_TRIANGLE_STRIP, drawindex, 4); 
}

glDisable(GL_TEXTURE_2D);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];

Should some of this be happening during initialization only?

Thanks, Scott

Scott
A: 

Scott, the TexParameter setup only needs to be done once per texture. However, that is not the source of your slowdown.

You'll be much better off building up a list of indexes, and calling glDrawArrays once for the entire set of tiles. The goal of vertex arrays are to allow you to draw as much as possible in one step.

glDrawTex should be avoided, because it forces you into the very inefficient one-at-a-time mindset.

Frogblast
Thanks, I knew there had to be a way to get it all done in one glDrawArrays call. After some research, I think I just have to use GL_QUADS and it'll know to create separate polygons. I'm away from my mac but I will test this later.
Scott
GL_QUADS are not supported in ES. You should use a pair of triangles. For this usage case, I wouldn't bother with trying FANs or STRIPs.
Frogblast
You're right, I ended up writing a couple triangles to simulate "quads". Thanks a lot. I got it going now and it seems to be pretty good, the slowdown is mostly gone.
Scott
A: 

What i did to speed my app up. Was after i load my level. I created an atlas out of the tiles on the map. Then every frame i check to see if the camera did move. If it did then i just pass an glTranslatef and move the entire map at once. If only dynamic objects move on the map then i just update that object in the vertex array atlas. This system is very effiecient as i am able to draw tons of tiles with no framerate drop.

A: 

Client states should be enabled only at initialization, also the glTexParameteri functions should be called when creating the texture object. All glEnable functions are not cached, meaning it will set the state even if it is already set to that value. All these small things can add up and slow you down.

BR

Istvan