LWJGL3 Best way to render 2D tiles

I started watching these tutorials for creating a 2d top-down game using LWJGL and I read that VBO’s should be fast but for rendering 48*48 tiles per frame I get only about 100FPS which is pretty slow because I will add a lot more stuff to the game than just some static, not moving or changing, tiles.

What can I do to make this faster? Keep in mind that I just started learning lwjgl and opengl so I probably won’t know many things.

Anyways, here are some parts of my code (I removed some parts from the code that were kinda meaningless and replaced them with some descriptions):

The main loop

double targetFPS = 240.0;
        double targetUPS = 60.0;

        long initialTime = System.nanoTime();
        final double timeU = 1000000000 / targetUPS;
        final double timeF = 1000000000 / targetFPS;
        double deltaU = 0, deltaF = 0;
        int frames = 0, updates = 0;
        long timer = System.currentTimeMillis();

        while (!window.shouldClose()) {
            long currentTime = System.nanoTime();
            deltaU += (currentTime - initialTime) / timeU;
            deltaF += (currentTime - initialTime) / timeF;
            initialTime = currentTime;

            if (deltaU >= 1) {
                // --- [ update ] ---
                --INPUT HANDLING FOR BASIC MOVEMENT, CLOSING THE GAME AND TURNING VSYNC ON AND OFF USING A METHOD FROM THE INPUT HANDLER CLASS--

                world.correctCamera(camera, window);

                window.update();

                updates++;
                deltaU--;
            }

            if (deltaF >= 1) {
                // --- [ render ] ---
                glClear(GL_COLOR_BUFFER_BIT);
                world.render(tileRenderer, shader, camera, window);
                window.swapBuffers();

                frames++;
                deltaF--;
            }
            --PRINTING THE FPS AND UPS EVERY SECOND--
        }

The input handler methods used:

I have this in my constructor:
this.keys = new boolean[GLFW_KEY_LAST];
    for(int i = 0; i < GLFW_KEY_LAST; i++)
        keys[i] = false;
And here are the methods: 
public boolean isKeyDown(int key) {
        return glfwGetKey(window, key) == 1;
    }
    public boolean isKeyPressed(int key) {
        return (isKeyDown(key) && !keys[key]);
    }
    public void update() {
        for(int i = 32; i < GLFW_KEY_LAST; i++)
            keys[i] = isKeyDown(i);
    }

This is the render method from the World class:

public void render(TileRenderer renderer, Shader shader, Camera camera, Window window) {
        int posX = ((int) camera.getPosition().x + (window.getWidth() / 2)) / (scale * 2);
        int posY = ((int) camera.getPosition().y - (window.getHeight() / 2)) / (scale * 2);
        for (int i = 0; i < view; i++) {
            for (int j = 0; j < view; j++) {
                Tile t = getTile(i - posX, j + posY);
                if (t != null)
                    renderer.renderTile(t, i - posX, -j - posY, shader, world, camera);
            }
        }
    }

This is the renderTile() method from TileRenderer:

public void renderTile(Tile tile, int x, int y, Shader shader, Matrix4f world, Camera camera) {
        shader.bind();
        if (tileTextures.containsKey(tile.getTexture()))
            tileTextures.get(tile.getTexture()).bind(0);

        Matrix4f tilePosition = new Matrix4f().translate(new Vector3f(x * 2, y * 2, 0));
        Matrix4f target = new Matrix4f();

        camera.getProjection().mul(world, target);
        target.mul(tilePosition);

        shader.setUniform("sampler", 0);
        shader.setUniform("projection", target);

        model.render();
    }

This is the constructor and render method from Model class:

public Model(float[] vertices, float[] texture_coords, int[] indices) {
        draw_count = indices.length;

        v_id = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, v_id);
        glBufferData(GL_ARRAY_BUFFER, createBuffer(vertices), GL_STATIC_DRAW);

        t_id = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, t_id);
        glBufferData(GL_ARRAY_BUFFER, createBuffer(texture_coords), GL_STATIC_DRAW);

        i_id = glGenBuffers();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_id);

        IntBuffer buffer = BufferUtils.createIntBuffer(indices.length);
        buffer.put(indices);
        buffer.flip();

        glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffer, GL_STATIC_DRAW);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    public void render() {
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

        glBindBuffer(GL_ARRAY_BUFFER, v_id);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

        glBindBuffer(GL_ARRAY_BUFFER, t_id);
        glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_id);
        glDrawElements(GL_TRIANGLES, draw_count, GL_UNSIGNED_INT, 0);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
    }

I store the vertices, texture coords and indices in the tile renderer:

float[] vertices = new float[]{
                -1f, 1f, 0, //top left     0
                1f, 1f, 0, //top right     1
                1f, -1f, 0, //bottom right 2
                -1f, -1f, 0, //bottom left 3
        };

        float[] texture = new float[]{
                0, 0,
                1, 0,
                1, 1,
                0, 1,
        };

        int[] indices = new int[]{
                0, 1, 2,
                2, 3, 0
        };

I don’t know what else to put here but the full source code and resources + shader files are available on github here.

You did provide everything necessary, no worries. Thanks for that! :slight_smile:

However, let me say this straight out: This is pretty much the least efficient and slowest possible way to do this.
You are re-uploading uniforms, resetting OpenGL state and performing a draw call for each and every single 4-vertices tile. This is super-duper extremely inefficient. :slight_smile:

You should/MUST batch multiple tiles in a single buffer and issue draw calls per batch and not per single tile. So, build a large 48*48 tiles vertex, index and texture coordinates buffer with the correct values and issue only one single draw call. This would be the fastest way to do it.

If at some point you need to alter the OpenGL state in between tiles of a single batch, for example when you have different shaders for different tiles, you need to sort by OpenGL state and create multiple batches per state.

Thing is, I’m a complete noob with opengl or anything related. I have just watched the tutorials and I don’t fully understand how half of the code works so I don’t know how to build the larger buffers containing multiple tiles. Could you help me with a basic example/some code?

Thank you very much for the answer! :slight_smile: