What is the correct way to draw a tilemap using OpenGL?

Started by
5 comments, last by _Silence_ 5 years, 2 months ago

 

Hi, I'm starting to learn OpenGL and it's been really fun because of the amount of things I can do. I would like to implement a tilemap in my game engine, but the way I'm trying to do this is consuming a lot of CPU and GPU, just to draw parts of the texture, without entities or animations. The approach I'm using is to load a texture with all the tiles (tileset) and traverse the image by drawing only parts of it in specific (side-by-side) positions in the visible region of the screen.

The OpenGL version is 3.3. And the code can be seen below.

I think there must be a right way to do it, because old games like Diablo 2 draw a big map with several things happening during the game and consume practically nothing, even playing for hours.

  


 void GameMap::draw()
    {
        // Use shader
        m_shader->use();

        // Use texture
        m_texture->bind();

        glBindVertexArray(m_quadVAO);

        // Draw map
        for (size_t r = 0; r < 10; r++)
        {
            for (size_t c = 0; c < 10; c++)
            {
                // Tile position
                m_tileCoord->setX(c*m_tileHeight);
                m_tileCoord->setY(r*m_tileHeight);

                // Isometric perspective
                m_tileCoord->convert2DToIso();

                // Draw tile by index
                drawTile(0);
            }
        }

        glBindVertexArray(0);
    }
    
    void GameMap::drawTile(GLint index)
    {
        // Identity matrix
        glm::mat4 position_coord = glm::mat4(1.0f);
        glm::mat4 texture_coord = glm::mat4(1.0f);
        
        // Part of the texture
        m_srcX = index * m_tileWidth;
        GLfloat clipX = m_srcX / m_texture->m_width;
        GLfloat clipY = m_srcY / m_texture->m_height;
        
        // Coordinates of the texture
        texture_coord = glm::translate(texture_coord, glm::vec3(clipX, clipY, 0.0f));

        // Coordinates of the vertices
        position_coord = glm::translate(position_coord, glm::vec3(m_tileCoord->getX(), m_tileCoord->getY(), 0.0f));
        position_coord = glm::scale(position_coord, glm::vec3(m_tileWidth, m_tileHeight, 1.0f));

        // Change the shader
        m_shader->setMatrix4("texture_coord", texture_coord);
        m_shader->setMatrix4("position_coord", position_coord);
        
        // Draws part of the texture
        glDrawArrays(GL_TRIANGLES, 0, 6);
    }

    void GameMap::initRenderData()
    {
        // Coordinates
        GLfloat posX = (GLfloat)m_tileWidth / m_texture->m_width;
        GLfloat posY = (GLfloat)m_tileHeight / m_texture->m_height;

        // Configure VAO/VBO
        GLuint VBO;
        GLfloat vertices[] = {
            // Left triangle
            // Pos      // Tex
            0.0f, 1.0f, 0.0f, posY, // Bottom left corner
            1.0f, 0.0f, posX, 0.0f, // Top right corner 
            0.0f, 0.0f, 0.0f, 0.0f,    // Upper left corner

            // Right triangle
            0.0f, 1.0f, 0.0f, posY, // Lower left corner
            1.0f, 1.0f, posX, posY, // Bottom right corner
            1.0f, 0.0f, posX, 0.0f // Top right corner 
        };

        glGenVertexArrays(1, &m_quadVAO);
        glGenBuffers(1, &VBO);

        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

        glBindVertexArray(m_quadVAO);

        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
    }
    
    // ------------------------------------------------------------------------------------------
    
    // Vertex Shader
    
    #version 330 core
    layout (location = 0) in vec4 vertex; // <vec2 position, vec2 texCoords>

    out vec4 TexCoords;

    uniform mat4 texture_coord;
    uniform mat4 position_coord;
    uniform mat4 projection;

    void main()
    {
        TexCoords =       texture_coord * vec4(vertex.z, vertex.w, 1.0, 1.0);
        gl_Position =   projection * position_coord * vec4(vertex.xy, 0.0, 1.0);
    }
    
    // Fragment Shader
    
    #version 330 core
    out vec4 FragColor;

    in vec4 TexCoords;

    uniform sampler2D image;

    void main()
    {
        FragColor =  texture(image, vec2(TexCoords.x, TexCoords.y));
    }


    print.thumb.png.f5dd1c6eabd5271f89292a1a6fc3982a.png
    
    
    
    
    

 

 

 

 

 

Advertisement

I think it's due to you issuing a drawcall for each tile, if you batch them all together in one drawcall it'll be much faster!

.:vinterberg:.

Look into instancing: https://learnopengl.com/Advanced-OpenGL/Instancing

 

 

"Those who would give up essential liberty to purchase a little temporary safety deserve neither liberty nor safety." --Benjamin Franklin

I have a similar problem.

I can tell you that drawing multiple primitives to achieve a tiled background
does not result in good performance.

But I still don't know if I should use:

  1. Instancing
  2. Rendering Layers to Screen-sized texture quads, then only render the visible ones (4 at max per layer)
  3. Create a mesh with UV coords per layer

I can tell you, that different games use either one of these approaches.
And normally you shouldn't run into performance problems on current computers,
when using any single one of these.

Still I would like to know which way is the best one.
I think I personally would take the second option.
It sounds like the best one regarding rendering time, but might result in higher memory usage,
when not using relatively hard to implement resource management.

Another possible approach would be to load the tilemap itself (as in the raw array of tile indexes) in one texture, the tiles images in another, and have a shader that reads the tile index from texture 1 and uses it to look up the tile image from texture 2.  This approach probably uses less video memory (and therefore less memory bandwidth) than any of the other approaches suggested so far.

I remember 6-7 years ago I was doing a little 2D RPG game with OpenGL. I was using old-fashion direct-mode (glBegin...) and using several textures per tile (each could be a different texture, meaning I did not use TexSubImage).

This was on a geforce 560. And this was very fast.

Think about it: in a screen you can see about 20*20 tiles (often less), with each is about 2 triangles, so 1200 triangles, nothing more ! Depth test is disabled, blending is enabled. Nothing more (or very few).

Of course if you're able to zoom out, then this will become slow. And then yes, use VAOs, use indexing, and for sure instancing will give you more performance.

This topic is closed to new replies.

Advertisement