0
\$\begingroup\$

I'm creating a game in which I renderer around 1000 to 2000 solid colored squares per frame, 90% of which are the same size: ~4 px depending on the dpi. I am looking for the most efficient way to render them all using libGDX for Android and PC. Here's my past two approaches:

  • ShapeRenderer: This worked alright but I would encounter medium lag with a 1000+ squares.
  • Meshes tutorial: This caused heavy lag with only 100+ squares.
\$\endgroup\$
1
\$\begingroup\$

I'm not sure if you went that far in improving the performance, but usually the best performance you get is the smaller number of ModelBuilder::part is called.

Since, each time a new material (color) is used, you have to create a new part, it's very good to group your squares by color and create one part for each color and then use multiple MeshPartBuilder::rect to draw all of the same color.

That way the CPU and GPU will have to synchronize only once for each color in your frame.

\$\endgroup\$
3
  • \$\begingroup\$ In my case there's around 500 colors at any moment which would lead me to believe that it would not help to group by color. Beyond that is there any other method of optimization? Or possibly more information on the topic? \$\endgroup\$ Feb 22 '16 at 19:43
  • \$\begingroup\$ Custom vertex/fragment shader is probably the best bet. \$\endgroup\$ Feb 23 '16 at 20:27
  • \$\begingroup\$ I could try adding a working example of that, when I have some free time. \$\endgroup\$ Feb 23 '16 at 20:39
1
\$\begingroup\$

Here is a working test class displaying 10,000 rectangles with random colors. The render loop takes <1ms.

private ShaderProgram shaderProgram;
private Mesh mesh;

@Override
public void create() {
    int numberOfRectangles = 10000;

    int numberOfVertices = 4 * numberOfRectangles;
    mesh = new Mesh(true, numberOfVertices, numberOfRectangles*6, VertexAttribute.Position(), VertexAttribute.ColorUnpacked());

    int vertexPositionValue = 3;
    int vertexColorValue = 4;
    Random rnd = new Random(0);
    int valuesPerVertex = vertexPositionValue + vertexColorValue;

    short[] vertexIndices = new short[numberOfRectangles*6];
    float[] verticesWithColor = new float[numberOfVertices * valuesPerVertex];

    for (int i = 0; i < numberOfRectangles; i++) {
        float colorR = generateNumberBetweenZeroAndOne(rnd);
        float colorG = generateNumberBetweenZeroAndOne(rnd);
        float colorB = generateNumberBetweenZeroAndOne(rnd);

        float x = generateNumberBetweenNegativeOneAndPointNine(rnd);
        float y = generateNumberBetweenNegativeOneAndPointNine(rnd);

        int rectangleOffsetInArray = i * valuesPerVertex * 4;

        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x, y, rectangleOffsetInArray, 0);
        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x + 0.1f, y, rectangleOffsetInArray, 1);
        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x + 0.1f, y + 0.1f, rectangleOffsetInArray, 2);
        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x, y+0.1f, rectangleOffsetInArray, 3);

        vertexIndices[i*6+0] = (short) (i*4+0);
        vertexIndices[i*6+1] = (short) (i*4+1);
        vertexIndices[i*6+2] = (short) (i*4+2);
        vertexIndices[i*6+3] = (short) (i*4+2);
        vertexIndices[i*6+4] = (short) (i*4+3);
        vertexIndices[i*6+5] = (short) (i*4+0);
    }
    mesh.setVertices(verticesWithColor);
    mesh.setIndices(vertexIndices);


    String vertexShader = "attribute vec4 a_position;    \n" +
            "attribute vec4 a_color;\n" +
            "varying vec4 v_color;" +
            "void main()                  \n" +
            "{                            \n" +
            "   v_color = a_color; \n" +
            "   gl_Position =  a_position;  \n"      +
            "}                            \n" ;
    String fragmentShader = "#ifdef GL_ES\n" +
            "precision mediump float;\n" +
            "#endif\n" +
            "varying vec4 v_color;\n" +
            "void main()                                  \n" +
            "{                                            \n" +
            "  gl_FragColor = v_color;\n" +
            "}";

    shaderProgram = new ShaderProgram(vertexShader, fragmentShader);
}

private void setValuesInArrayForVertex(float[] verticesWithColor, float colorR, float colorG, float colorB, float x, float y, int rectangleOffsetInArray, int vertexNumberInRect) {
    int vertexOffsetInArray = rectangleOffsetInArray + vertexNumberInRect * 7;
    // x position
    verticesWithColor[vertexOffsetInArray + 0] = x;
    // y position
    verticesWithColor[vertexOffsetInArray + 1] = y;
    // z position (screen coordinates)
    verticesWithColor[vertexOffsetInArray + 2] = 0;
    // red value
    verticesWithColor[vertexOffsetInArray + 3] = colorR;
    // green value
    verticesWithColor[vertexOffsetInArray + 4] = colorG;
    // blue value
    verticesWithColor[vertexOffsetInArray + 5] = colorB;
    // alpha value
    verticesWithColor[vertexOffsetInArray + 6] = 1f;
}

private float generateNumberBetweenNegativeOneAndPointNine(Random rnd) {
    return rnd.nextFloat() * 1.9f - 1f;
}

private float generateNumberBetweenZeroAndOne(Random rnd) {
    return rnd.nextFloat();
}

@Override
public void render() {
    long start = System.currentTimeMillis();
    Gdx.gl.glClearColor(0, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

    shaderProgram.begin();
    mesh.render(shaderProgram, GL20.GL_TRIANGLES);
    shaderProgram.end();

    System.out.println("render() took: "+(System.currentTimeMillis()-start)+"ms");
}

@Override
public void dispose() {
    mesh.dispose();
}

@Override
public void pause() {

}

@Override
public void resize(int width, int height) {

}

@Override
public void resume() {

}

Even if you were to generate mesh for each frame, the time is <=5ms on my Mac, here is an example (Warning!!! - flashing images - epilepsy attach may be induced):

private ShaderProgram shaderProgram;
private Mesh mesh;
private Random rnd = new Random(0);

@Override
public void create() {
    String vertexShader = "attribute vec4 a_position;    \n" +
            "attribute vec4 a_color;\n" +
            "varying vec4 v_color;" +
            "void main()                  \n" +
            "{                            \n" +
            "   v_color = a_color; \n" +
            "   gl_Position =  a_position;  \n" +
            "}                            \n";
    String fragmentShader = "#ifdef GL_ES\n" +
            "precision mediump float;\n" +
            "#endif\n" +
            "varying vec4 v_color;\n" +
            "void main()                                  \n" +
            "{                                            \n" +
            "  gl_FragColor = v_color;\n" +
            "}";

    shaderProgram = new ShaderProgram(vertexShader, fragmentShader);
}

private void generateMesh() {
    if (mesh != null)
        mesh.dispose();

    int numberOfRectangles = 10000;

    int numberOfVertices = 4 * numberOfRectangles;
    mesh = new Mesh(true, numberOfVertices, numberOfRectangles * 6, VertexAttribute.Position(), VertexAttribute.ColorUnpacked());

    int vertexPositionValue = 3;
    int vertexColorValue = 4;

    int valuesPerVertex = vertexPositionValue + vertexColorValue;

    short[] vertexIndices = new short[numberOfRectangles * 6];
    float[] verticesWithColor = new float[numberOfVertices * valuesPerVertex];

    for (int i = 0; i < numberOfRectangles; i++) {
        float colorR = generateNumberBetweenZeroAndOne(rnd);
        float colorG = generateNumberBetweenZeroAndOne(rnd);
        float colorB = generateNumberBetweenZeroAndOne(rnd);

        float x = generateNumberBetweenNegativeOneAndPointNine(rnd);
        float y = generateNumberBetweenNegativeOneAndPointNine(rnd);

        int rectangleOffsetInArray = i * valuesPerVertex * 4;

        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x, y, rectangleOffsetInArray, 0);
        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x + 0.1f, y, rectangleOffsetInArray, 1);
        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x + 0.1f, y + 0.1f, rectangleOffsetInArray, 2);
        setValuesInArrayForVertex(verticesWithColor, colorR, colorG, colorB, x, y + 0.1f, rectangleOffsetInArray, 3);

        vertexIndices[i * 6 + 0] = (short) (i * 4 + 0);
        vertexIndices[i * 6 + 1] = (short) (i * 4 + 1);
        vertexIndices[i * 6 + 2] = (short) (i * 4 + 2);
        vertexIndices[i * 6 + 3] = (short) (i * 4 + 2);
        vertexIndices[i * 6 + 4] = (short) (i * 4 + 3);
        vertexIndices[i * 6 + 5] = (short) (i * 4 + 0);
    }
    mesh.setVertices(verticesWithColor);
    mesh.setIndices(vertexIndices);
}

private void setValuesInArrayForVertex(float[] verticesWithColor, float colorR, float colorG, float colorB, float x, float y, int rectangleOffsetInArray, int vertexNumberInRect) {
    int vertexOffsetInArray = rectangleOffsetInArray + vertexNumberInRect * 7;
    // x position
    verticesWithColor[vertexOffsetInArray + 0] = x;
    // y position
    verticesWithColor[vertexOffsetInArray + 1] = y;
    // z position (screen coordinates)
    verticesWithColor[vertexOffsetInArray + 2] = 0;
    // red value
    verticesWithColor[vertexOffsetInArray + 3] = colorR;
    // green value
    verticesWithColor[vertexOffsetInArray + 4] = colorG;
    // blue value
    verticesWithColor[vertexOffsetInArray + 5] = colorB;
    // alpha value
    verticesWithColor[vertexOffsetInArray + 6] = 1f;
}

private float generateNumberBetweenNegativeOneAndPointNine(Random rnd) {
    return rnd.nextFloat() * 1.9f - 1f;
}

private float generateNumberBetweenZeroAndOne(Random rnd) {
    return rnd.nextFloat();
}

@Override
public void render() {
    long start = System.currentTimeMillis();

    generateMesh();

    Gdx.gl.glClearColor(0, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

    shaderProgram.begin();
    mesh.render(shaderProgram, GL20.GL_TRIANGLES);
    shaderProgram.end();

    System.out.println("render() took: " + (System.currentTimeMillis() - start) + "ms");
}

@Override
public void dispose() {
    mesh.dispose();
}

@Override
public void pause() {

}

@Override
public void resize(int width, int height) {

}

@Override
public void resume() {

}
\$\endgroup\$
9
  • \$\begingroup\$ If you have any questions about the code, please comment here. \$\endgroup\$ Feb 23 '16 at 22:53
  • \$\begingroup\$ I have modified this to draw to a List first and it still runs flawlessly! What is the function of the following part (along with the 5 lines after): indices[i*6+0] = (short) (i*4+0); \$\endgroup\$ Feb 24 '16 at 1:58
  • 1
    \$\begingroup\$ Each rectangle is two triangles, the 6 lines create the two triangles (3 vertices each). The lines before define 4 vertices, the 6 lines you asked for define the indices of those 4 vertices specified before. \$\endgroup\$ Feb 25 '16 at 11:12
  • \$\begingroup\$ Thank's for all the help :). However, I'm experiencing lag with this solution when shapeRenderer is working. Here's a video: vid.me/xPl5 and here's my code snippet: gist.github.com/Wolfgange3311999/e6dca279d3b96a31703a \$\endgroup\$ Feb 26 '16 at 2:36
  • 1
    \$\begingroup\$ Also, try using LinkedLIst instead of ArrayList for your "rectangles". Just make sure you iterate over it with Iterator (or for (Rect t : rectangles)). Alternatively, you could specify a large initial size for your ArrayList, rather than default. You most likely resize it A LOT, if you draw a lot of rectangles. \$\endgroup\$ Feb 26 '16 at 23:47
1
\$\begingroup\$

I'm not familiar with LibGDX or openGL so I can't provide code for you, but I believe what you are wanting is possible and called instanced drawing.

Essentially, you'll bind 2 (or more) vertex buffers:

  1. Vertex buffer - A unit square
    (0,0) (1,0) (0,1) and (1,1)

  2. Instance buffer
    WorldMatrix + Color for each instance

Using the appropriate instanced draw command, the GPU will render the same 4 vertices over and over, automatically swapping the WorldMatrix and Color for each. You can use multiple instance buffers to render more objects than will fit in a single one. The size of each instance I've suggested is 5 float4's == 80bytes. For comparison, the memory footprint that a typical 2048x2048 RGBA texture occupies (about 67MB) would hold about 838,000 instances. Once your instance count goes past 100k, your framerate may start dropping.... LOL!

//For 14567 instances
SetInstanceBuffer1(); //First 10000
DrawInstanced(10000);
SetInstanceBuffer2(); //remainder
DrawInstanced(4567);
SetInstanceBuffer3(); //100 frequently updated objects
//InstanceBuffer3 was created small so updating it is very fast
DrawInstanced(100);

If you'll be frequently updating one part of the buffer, but not the other, i.e. colors change per frame, but positions are usually the same, you can split one buffer into two. As such, you could upload a new color buffer every frame without having to upload a bunch of unchanged WorldMatrices.

//14567 instances (world matrix + color) occupies about 1.16Mb
//Smaller buffers means faster updates

SetUnitSquareVertexBufferToSlot1(); //4 vertices
SetPerInstanceWorldMatrixBufferToSlot2(); //14567 matrices
//14567 world matrices occupies about 932Kb
SetPerInstanceColorBufferToSlot3(); //14567 colors
//14567 colors occupies about 233Kb
DrawInstanced(14567);

Edit:
http://learnopengl.com/#!Advanced-OpenGL/Instancing

It's not specific to LibGDX; I leave it to you to understand where/how to integrate this into your code.

Geometry shader version:
Feed the GPU a single vertex buffer. Each vertex contains the top-left corner of each square, width, height, and color. The geometry shader emits the same four vertices for each input vertex:

(Position.X, Position.Y)
(Position.X + Width, Position.Y)
(Position.X, Position.Y + Height)
(Position.X + Width, Position.Y + Height)

As such, each input vertex is an "instance". As with instancing, each input vertex may be assembled from multiple buffers/slots, again, allowing you to update the color buffer without uploading redundant information.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.