0
\$\begingroup\$

I'm trying to make a game which behaves somewhat like The Powder Toy. The world is square and can be filled with particles. Particles are drawn as small squares, constrained to integer positions, and stored in a 2D array, similar to the GOL. Here's what the world looks like: sample image of game I've tried a few different approaches of drawing particles to the screen:

  • Using a ShapeRenderer by calling setColor and rect for each particle. Seems to have been the slowest of the approaches.
  • Using a SpriteBatch to draw a Pixmap for each particle.
  • Writing asynchronously to a Pixmap, storing the Pixmap in a Texture, then drawing the Pixmap with a SpriteBatch.

Although these approaches work, I'd be interested in hearing about other, possibly more efficient ways to render the particles. Could VBO be used here? Or is it not accessible from within gdx?

\$\endgroup\$
1
\$\begingroup\$

After pilfering through some of the information on the LIBGDX documentation pages, I stumbled across something called a mesh, which looked promising. This page provided further information about drawing primitive shapes using meshes. By combining what I had learned from the two above sources, I was able to draw the points much more efficiently using GL_POINT to draw the mesh. Here's a file that you might copy to test this out and understand the approach:

package com.mygdx.game;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.MathUtils;

import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL32.GL_PROGRAM_POINT_SIZE;

public class MyGdxGame extends ApplicationAdapter {
    private static final int NUM_POINTS = 100_000;
    private OrthographicCamera camera;
    private Mesh mesh;
    private ShaderProgram shader;
    private int values_per_vertex;

    @Override
    public void create() {
        camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        camera.update();

        mesh = new Mesh(true, NUM_POINTS, 0, new VertexAttribute(VertexAttributes.Usage.Position, 2, "a_position"),
                new VertexAttribute(VertexAttributes.Usage.ColorUnpacked, 3, "a_color"));

        values_per_vertex = 2 + 3; // corresponds with the values given to the VertexAttribute constructor for size

        final String vertex_shader = "attribute vec2 a_position;\n" +
                "attribute vec3 a_color;\n" +
                "varying vec3 v_color;\n" +
                "uniform mat4 u_proj;\n" +
                "void main() {\n" +
                "   v_color = a_color;\n" +
                "   gl_Position = u_proj * vec4(a_position, 0.0, 1.0);\n" +
                "   gl_PointSize = abs(sin(a_position / 200)) * 10;" +
                "}\n";
        final String fragment_shader = "varying vec3 v_color;" +
                "void main() {\n" +
                "   gl_FragColor = vec4(v_color, 1.0);\n" +
                "}";

        ShaderProgram.pedantic = true;
        shader = new ShaderProgram(vertex_shader, fragment_shader);
        System.out.println(shader.getLog());

        glEnable(GL_PROGRAM_POINT_SIZE); // allow the vertex shader to specify the size of the point
        // glPointSize(int x); can be used if you don't want to define the size per-point
    }

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

        // generate an array of information about each vertex to be passed to the mesh and then the shader
        final float[] vertices = new float[NUM_POINTS * values_per_vertex];

        for (int i = 0; i < NUM_POINTS * values_per_vertex; i += values_per_vertex) {
            // x value should be within the valid x range for the camera
            vertices[i] = (int) MathUtils.random(-Gdx.graphics.getWidth() / 2f, Gdx.graphics.getWidth() / 2f);
            // same idea with the y value
            vertices[i + 1] = (int) MathUtils.random(-Gdx.graphics.getHeight() / 2f, Gdx.graphics.getHeight() / 2f);
            // r component of color
            vertices[i + 2] = MathUtils.random();
            // g component of color
            vertices[i + 3] = MathUtils.random();
            // b component of color
            vertices[i + 4] = MathUtils.random();
        }

        final long start = System.nanoTime();
        mesh.setVertices(vertices); // send our array of vertex information to the mesh

        camera.viewportWidth = Gdx.graphics.getWidth();
        camera.viewportHeight = Gdx.graphics.getHeight();
        camera.update();

        shader.begin();
        shader.setUniformMatrix("u_proj", camera.combined); // use the camera's perspective when drawing
        mesh.render(shader, GL20.GL_POINTS); // draw the points in the mesh
        shader.end();

        System.out.println("transfer and render took: " + (System.nanoTime() - start) / 1e9 + " s");
    }

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

Here's an image of the output: 100,000 points render blazingly fast using this method

When using this method and changing vertex locations each frame, I've found the majority consumer of time per-render to be the population of the vertex array which is sent to the GPU for rendering. Even for 1,000,000 points, my mid-range graphics card can chug them out in under 16 ms, but the CPU struggles mightily to populate an array of 5,000,000 floats in a short (under 16 ms) amount of time. It seems that any 'optimizations' of this approach would do best to focus on the population of the vertex array.

\$\endgroup\$
0
\$\begingroup\$

I've also had the same problem recently and the way I did it, is similar to what you've already listed there. Let's take this as example:

final int scale = 4;
final int width = 600 / scale;
final int height = 400 / scale;
// Called only once in the beginning
public void setup()
{
    int[][] arrayOfPixels = new int[width][height];
    for(int x = 0; x < width; x++)
    {
        for(int y = 0; y < height; y++)
        {
            arrayOfPixels[x][y] = -1;
        }
    }
}

// Called every frame
public void render()
{
    for(int x = 0; x < width; x++)
    {
        for(int y = 0; y < height; y++)
        {
            if(arrayOfPixels[x][y] == -1)
                continue;

            shapeRenderer.setColor(arrayOfPixels[x][y]);
            shapeRenderer.rect(x * scale, y * scale, scale, scale);
        }
    }

    // Do whatever updating the pixels need to do.
    // If you want you can also replace the int
    // array with an array of something else that
    // contains the color of the pixel
}

What this does is it keeps the pixel type of rendering but keeps it lightweight because of the scale. It'll look as it's single pixel but it'll run (scale * scale) times faster.

\$\endgroup\$
  • \$\begingroup\$ How long does it take render to run for you, using PerformanceCounter? \$\endgroup\$ – paleto-fuera-de-madrid Aug 28 '17 at 0:45
  • \$\begingroup\$ @paleto-fuera-de-madrid I haven't had a chance to really test it but with a scale of 5, it runs with more than 100 fps on my Google Pixel \$\endgroup\$ – Hashim Kayani Aug 28 '17 at 0:47
0
\$\begingroup\$

I've had good luck with using one FrameBuffer per chunk. Then I draw to the FrameBuffer with 1-pixel sprites or ShapeRenderer. That way you can mark chunks as dirty and only update them as needed. Or limit the number of chunks updated per frame.

\$\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.