2D and performance (using textures)

Started by
21 comments, last by Boder 16 years, 1 month ago
So, I wrote my own cute little 2D tile-based engine that works fine as far as I want it for now :). Only one point is nagging me: is has a FPS of 68. What it does: it draws 9*9 (and 10*10 if the character is moving) quads, each having its own texture. The texture and the quad are both 64 pixels in width and height. It determines what texture is needed via an array containing numbers indicating what texture to be used on certain positions. I did some research, and found out that the drawing part took 1000 times more time then anything else together I do per frame...that is nagging me ^^. How can I optimise my code? I heard about using the GL_TEXTURE_RECTANGLE_EXT extension...but that didn't really do much (nothing to be honest). The relevant code: The main function:
int main()
{
    Graphics graphics(512, 512);    

    int frames = 1;
    bool running = true;
    while(running)
    {
        graphics.resize();

        glfwSetCharCallback(handle_keyboard);

        graphics.draw(); // takes WAY too much time

        running = !glfwGetKey(GLFW_KEY_ESC) && glfwGetWindowParam(GLFW_OPENED);
        frames++;
    }
    return 0;
}

The class:
class Graphics
{
    int width, height, old_width, old_height;
    GLuint textures[n_textures];

    void draw_rect(GLuint, int, int, int, int);

    public:
        Graphics(int, int);
        ~Graphics();
        bool init(int, int);
        void resize();
        void draw();
};

Graphics::Graphics(int width, int height)
{
    glewInit();
    glfwInit();
    if(!glfwOpenWindow(width, height, 0, 0, 0, 0, 0, 0, GLFW_WINDOW))
    {
        return;
    }
    glfwSetWindowTitle("GLFW Application");

    old_width = old_height = 0;
    resize();

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    //glEnable(GL_TEXTURE_2D);
    glEnable(GL_TEXTURE_RECTANGLE_EXT);

    corona::Image *image;
    for (int i = 0; i < n_textures; i++)
    {
        image = corona::OpenImage(texture_paths);
        if (!image)
        {
            printf("Error: image error");
            return;
        }

        glGenTextures(1, &textures);
        glBindTexture(GL_TEXTURE_RECTANGLE_EXT, textures);

        //corona::PixelFormat format = image->getFormat();

        glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameterf(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_T, GL_REPEAT);

        glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, 0, GL_RGB, image->getWidth(), image->getHeight(), 0, GL_RGB, GL_UNSIGNED_BYTE, image->getPixels());
    }
    delete image;
}

Graphics::~Graphics()
{
    glDeleteTextures(n_textures, textures);
    glfwTerminate();
}

void Graphics::resize()
{    
    glfwGetWindowSize(&width, &height);

    if (width != old_width || height != old_height)
    {
        height = height > 0 ? height : 1;

        glViewport(0, 0, width, height);

        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0.0f, TILE_SIZE*TILES, TILE_SIZE*TILES, 0.0f, -1.0f, 1.0f);

        glMatrixMode(GL_MODELVIEW);

        old_width = width;
        old_height = height;
    }
}

void Graphics::draw()
{
    glClear(GL_COLOR_BUFFER_BIT);
    glLoadIdentity();

    for (int y = 0; y < 9; y++)
    {
        for (int x = 0; x < 9; x++)
        {
            draw_rect(textures[map[y][x]], x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
            x++;
        }
        y++;
    }
    glfwSwapBuffers();
}

void Graphics::draw_rect(GLuint texture, int x, int y, int w, int h)
{
    glBindTexture(GL_TEXTURE_RECTANGLE_EXT, texture);
    glBegin(GL_QUADS);
        glTexCoord2i(0, 0); glVertex2i(x, y);
        glTexCoord2i(64, 0); glVertex2i(x+w, y);
        glTexCoord2f(64, 64); glVertex2i(x+w, y+h);
        glTexCoord2f(0, 64); glVertex2i(x, y+h);
    glEnd();
}

Global variables:
const int n_textures = 2;
char *texture_paths[n_textures] = {"data/dirt.png", "data/grass.png"};

int map[18][18] = {
    {0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0}
};

Thank you :)
[size="2"]SignatureShuffle: [size="2"]Random signature images on fora
Advertisement
You'll want to batch your draw calls, that is draw everything with the same texture at once since switching textures is expensive. You might also want to not use immediate mode and probably should only be calling glBegin/End once per frame.
GL_TEXTURE_RECTANGLE_ARB is not faster, but it is to be used in cases where you for example need a texture which fits the screen resolution perfectly (like 1600x1200).
So put GL_TEXTURE_2D back in your code. [smile]

Looking through your code I can't see any big mistakes, I haven't used GLFW myself though, and don't know anything about it.

However, you'll want to make as few glBindTexture() and glBegin() calls as possible, so you should render those tiles that share the same texture object in succession. If you have just two textures, you'll need just two glBegin() calls and two glBindTexture() calls. This will speed things up significantly.

I hope this helps.
while (tired) DrinkCoffee();
68 FPS?, you sure its not got VSync enabled?
Ok thanks :). I rewrote a bit so it would only call glBegin()/glEnd/glBindTexture once...added 2 or 3 frames per second (its only a small map).
Quote:Original post by Neutrinohunter
68 FPS?, you sure its not got VSync enabled?
Ok, I looked in the GLFW user's manual, I set the buffer swap interval to zero, ran the program and WOW: FPS gets to 4200!
The guide also says:
Quote:Using a swap interval of zero can be useful for benchmarking purposes, when it is not desirable to measure the time it takes to wait for the vertical retrace. However, a swap interval of 1 generally gives better visual quality.
Should I have an interval or not? With interval means low FPS but they say better quality...is that worth it?

Thank you guys :)
[size="2"]SignatureShuffle: [size="2"]Random signature images on fora
It means that it will update with the screen refresh on the vertical refresh rate but that should be fine not to have Vysnc enabled, most of the time I don't see any difference. Though one would say if your progam goes lower than around 30FPS you will see a problem.

Neutrinohunter
Quote:
The guide also says:
Quote:Using a swap interval of zero can be useful for benchmarking purposes, when it is not desirable to measure the time it takes to wait for the vertical retrace. However, a swap interval of 1 generally gives better visual quality.
Should I have an interval or not? With interval means low FPS but they say better quality...is that worth it?

Enabling v-sync simply means that the buffer-swap in double-buffered rendering in synchronized with monitor's vertical refresh (which is occuring at 60-100Hz usually). Normally, even if your scene is rendering at 4000fps, your monitor can still only display 60-100 frames per second. Enabling v-sync prevents tearing (buffer swap occurring in middle of monitor drawing your image), and the only reason for disabling it (apart from benchmarking) is when your scene renders at lower fps than the monitor refresh (lets say 58fps with 60Hz monitor) because then you miss one refresh and have to wait for another, effectively lowering the actual fps up to half (so you got 30fps instead of 59 in worst case).

Basically this is the option that should be left to user to choose (also note that some drivers have the v-sync enabled by default).
While it sounds like you don't really have a performance problem after all, one relatively easy "optimization" to do in tile based games is putting many (or all) of the tiles in one texture. For example, a 512x512 texture can hold 64 tiles of your current tile size. This means you could render all your tile quads within one glBegin/glEnd pair.

You probably shouldn't worry about optimizations at this point though; making the gameplay happen is more important.
Quote:Original post by Fingers_
You probably shouldn't worry about optimizations at this point though; making the gameplay happen is more important.


Very true ;), but I don't want to discover that when I finished it, it is slow as hell ^^ and that I'd have to rewrite it all =/ lol

Quote:Original post by Fingers_
While it sounds like you don't really have a performance problem after all, one relatively easy "optimization" to do in tile based games is putting many (or all) of the tiles in one texture. For example, a 512x512 texture can hold 64 tiles of your current tile size. This means you could render all your tile quads within one glBegin/glEnd pair.


That's only for loading the textures isn't it? As I only load them once it wouldn't do much.
And if you mean the drawing, the map can be different each time, as the player moves through it (it has scrolling).

Though...it would improve performance if I didn't redraw the scene if it wasn't needed.

Can I somehow "save" a buffer (the map, not the object which move constantly) so I can keep using it if the character doesn't move? (character is always centered, so when it moves the map has to be redrawn)

[size="2"]SignatureShuffle: [size="2"]Random signature images on fora
That's only for loading the textures isn't it? As I only load them once it wouldn't do much.
And if you mean the drawing, the map can be different each time, as the player moves through it (it has scrolling).

I think what he means is that you can avoid glBindTexture's in this way. You can just use glTexCoord to draw a different texture on a quad. Pretty smart, but a little more complex I guess.

This topic is closed to new replies.

Advertisement