Sprite batching for Monkeysheet

I’m doing this and nothing gets rendered:

    public static int SIZE = 2;

        for (int i = 0; i < SIZE; i++) {
            allMyColors[i] = ColorRGBA.randomColor();
            if (USE_CUSTOMMESH) {
        vertices[0+SIZE*4] = new Vector3f(0, 0, 0);
        vertices[1+SIZE*4] = new Vector3f(3+SIZE*4, 0, 0);
        vertices[2+SIZE*4] = new Vector3f(0, 3+SIZE*4, 0);
        vertices[3+SIZE*4] = new Vector3f(3+SIZE*4, 3+SIZE*4, 0);
        texCoord[0+SIZE*4] = new Vector2f(0, 0);
        texCoord[1+SIZE*4] = new Vector2f(1+SIZE*4, 0);
        texCoord[2+SIZE*4] = new Vector2f(0, 1+SIZE*4);
        texCoord[3+SIZE*4] = new Vector2f(1+SIZE*4, 1+SIZE*4);
}
        int[] indexes = {2, 0, 1, 1, 3, 2,
                         6, 0, 5, 5, 7, 6};
        mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
        mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord));
        mesh.setBuffer(Type.Index, 3, BufferUtils.createIntBuffer(indexes));
        mesh.updateBound();
            int i=0;
            geom =new Geometry("Box", mesh);
                mesh.setBuffer(VertexBuffer.Type.Color, 4, new float[]{allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a});
            Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
            mat.setBoolean("VertexColor", true);
            geom.scale(.05f);
            geom.setMaterial(mat);                   // set the cube's material
            rootNode.attachChild(geom);



What am I doing wrong? Thanks!

Well… this acts like you are trying to make your own batch or something but then the indexes are all fixed instead of using i or anything.

Honestly, I have trouble figuring out even what you really mean to do here but each loop pass will be overwriting the same four elements.

1 Like

That was it! Now I’d like to manipulate the vertex buffer more efficiently than this:

                mesh.setBuffer(VertexBuffer.Type.Color, 4, new float[]{allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a});

I’ve seen there is VertexBuffer.setElementComponent(), but maybe there is some more direct approach, like arrayCopy() or something?

I don’t know, man… I can only see part of your code and it looks like some buffers you are building batched but the color buffer you aren’t. I’m not sure what you aren’t doing it like every other buffer.

If you are just talking about how you update the elements in the middle, just grab the FloatBuffer from the VertexBuffer and put whatever you want in it. There are numerous examples of this all over JME if you just look in the code. You could even look inside BatchNode to see what it’s doing to update just part of the buffer.

1 Like

BufferUtils was an important class back then when I used it. It allowed me to put in values without using those temporary arrays that you are using now.

1 Like

Mostly there but… colors don’t get updated in real time. Could you please have a look at the simpleupdate? Thanks!

public class TestBatch extends SimpleApplication {

    public static void main(String[] args) {
        TestBatch app = new TestBatch();
        app.start(); // start the game
    }
    BatchNode bn = new BatchNode();

    public static int SIZE = 2;
    public static boolean USE_BATCHNODE = false;
    public static boolean USE_CUSTOMMESH = true;

    ColorRGBA[] allMyColors = new ColorRGBA[SIZE];
    Quad allQuads[] = new Quad[SIZE];
    VertexBuffer vb[] = new VertexBuffer[SIZE];
    FloatBuffer colorBuffer;
    Mesh mesh;

    @Override
    public void simpleInitApp() {

        mesh = new Mesh();
        Vector3f[] vertices = new Vector3f[40000];
        /*vertices[0] = new Vector3f(0, 0, 0);
        vertices[1] = new Vector3f(3, 0, 0);
        vertices[2] = new Vector3f(0, 3, 0);
        vertices[3] = new Vector3f(3, 3, 0);*/
        Vector2f[] texCoord = new Vector2f[40000];
        /*texCoord[0] = new Vector2f(0, 0);
        texCoord[1] = new Vector2f(1, 0);
        texCoord[2] = new Vector2f(0, 1);
        texCoord[3] = new Vector2f(1, 1);*/
        int[] indexes = {2, 0, 1, 1, 3, 2,
            6, 4, 5, 5, 7, 6};

        for (int i = 0; i < SIZE; i++) {
            allMyColors[i] = ColorRGBA.randomColor();
            if (USE_CUSTOMMESH) {
                vertices[0 + i * 4] = new Vector3f(i * 4, 0, 0);
                vertices[1 + i * 4] = new Vector3f(3 + i * 4, 0, 0);
                vertices[2 + i * 4] = new Vector3f(i * 4, 3, 0);
                vertices[3 + i * 4] = new Vector3f(3 + i * 4, 3, 0);
                texCoord[0 + i * 4] = new Vector2f(0, 0);
                texCoord[1 + i * 4] = new Vector2f(1 + i * 4, 0);
                texCoord[2 + i * 4] = new Vector2f(0, 1 + i * 4);
                texCoord[3 + i * 4] = new Vector2f(1 + i * 4, 1 + i * 4);
            } else {
                allQuads[i] = new Quad(.05f, .05f); // create cube shape
                geom = new Geometry("Box", allQuads[i]);  // create cube geometry from the shape
                allQuads[i].setBuffer(VertexBuffer.Type.Color, 4, new float[]{allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a});
                vb[i] = allQuads[i].getBuffer(Color);
                Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
                mat.setBoolean("VertexColor", true);
                geom.setMaterial(mat);                   // set the cube's material
                geom.move(-5, -5, 0);
                int j = i / 100;
                int k = i - j * 100;
                geom.move(k / 10f, j / 10f, 0);
                if (USE_BATCHNODE) {
                    bn.attachChild(geom);              // make the cube appear in the scene
                } else {
                    rootNode.attachChild(geom);
                }
            }
        }
        if (USE_CUSTOMMESH) {
            mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
            mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord));
            mesh.setBuffer(Type.Index, 3, BufferUtils.createIntBuffer(indexes));
            mesh.updateBound();
            int i = 0;
            geom = new Geometry("Box", mesh);
            mesh.setBuffer(VertexBuffer.Type.Color, 4, new float[16 * SIZE]);
            colorBuffer = (FloatBuffer) mesh.getBuffer(Color).getData();
            Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
            mat.setBoolean("VertexColor", true);
            geom.scale(.05f);
            geom.setMaterial(mat);                   // set the cube's material
            rootNode.attachChild(geom);
        }
        if (USE_BATCHNODE) {
            bn.batch();
            rootNode.attachChild(bn);
        }
    }
    Geometry geom;

    float cc[] = new float[16];

    public void simpleUpdate(float tpf) {
        for (int i = 0; i < SIZE; i++) {
            allMyColors[i] = ColorRGBA.randomColor();

            if (USE_CUSTOMMESH) {
                /*mesh.setBuffer(VertexBuffer.Type.Color, 4, new float[]{allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a});*/
                colorBuffer.position(i * 16);
                cc[0] = allMyColors[i].r;
                cc[1] = allMyColors[i].g;
                cc[2] = allMyColors[i].b;
                cc[3] = allMyColors[i].a;
                cc[4] = allMyColors[i].r;
                cc[5] = allMyColors[i].g;
                cc[6] = allMyColors[i].b;
                cc[7] = allMyColors[i].a;
                cc[8] = allMyColors[i].r;
                cc[9] = allMyColors[i].g;
                cc[10] = allMyColors[i].b;
                cc[11] = allMyColors[i].a;
                cc[12] = allMyColors[i].r;
                cc[13] = allMyColors[i].g;
                cc[14] = allMyColors[i].b;
                cc[15] = allMyColors[i].a;
                colorBuffer.put(cc, 0, 16);
            } else {
                allQuads[i].setBuffer(VertexBuffer.Type.Color, 4, new float[]{allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                    allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a});
            }
            //VertexBuffer vb[allQuads[i].getBuffer(Type.Color);
            /*vb[i].setElementComponent(i, 4, new float[]{allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a,
                allMyColors[i].r, allMyColors[i].g, allMyColors[i].b, allMyColors[i].a});*/
        }
        if (USE_BATCHNODE) {
            bn.onGeometryUnassociated(geom);
            bn.batch();
        }
    }

}

In the case of a custom mesh, you aren’t even setting the colors.

Edit… actually I see it in simpleUpdate() now but it’s kind of confusing.

You never tell the VertexBuffer that its data has been updated. At the end of your loop try calling setBuffer(colorBuffer) or tell the VertexBuffer that it’s data has been changed.

1 Like

Awesome! Pushing further, I get 200000 quads at 60FPS… and what is slowing me down is the for loop that iterates on all the quads for each cycle.

is it possible to go higher than that? :wink:

Code:

public class TestBatch extends SimpleApplication {

    public static void main(String[] args) {
        TestBatch app = new TestBatch();
        app.start(); // start the game
    }
    BatchNode bn = new BatchNode();

    public static int SIZE = 200000;
    public static boolean USE_CUSTOMMESH = true;

    ColorRGBA[] allMyColors = new ColorRGBA[SIZE];
    VertexBuffer vb[] = new VertexBuffer[SIZE];
    FloatBuffer colorBuffer;
    Mesh mesh;

    @Override
    public void simpleInitApp() {

        mesh = new Mesh();
        Vector2f[] quads = new Vector2f[SIZE];
        Vector3f[] vertices = new Vector3f[4 * SIZE];
        Vector2f[] texCoord = new Vector2f[4 * SIZE];
        int[] indexes = new int[6 * SIZE];

        for (int i = 0; i < SIZE; i++) {
            int j = i / 1000;
            int k = i - j * 1000;
            quads[i] = new Vector2f(k / 10f, j / 10f);
            allMyColors[i] = ColorRGBA.randomColor();
            if (USE_CUSTOMMESH) {
                vertices[0 + i * 4] = new Vector3f(k * 4, j * 4, 0);
                vertices[1 + i * 4] = new Vector3f(3 + k * 4, j * 4, 0);
                vertices[2 + i * 4] = new Vector3f(k * 4, 3 + j * 4, 0);
                vertices[3 + i * 4] = new Vector3f(3 + k * 4, 3 + j * 4, 0);
                texCoord[0 + i * 4] = new Vector2f(0, 0);
                texCoord[1 + i * 4] = new Vector2f(1 + k * 4, 0);
                texCoord[2 + i * 4] = new Vector2f(0, 1 + k * 4);
                texCoord[3 + i * 4] = new Vector2f(1 + k * 4, 1 + j * 4);
                indexes[6 * i] = 2 + 4 * i;
                indexes[1 + 6 * i] = 0 + 4 * i;
                indexes[2 + 6 * i] = 1 + 4 * i;
                indexes[3 + 6 * i] = 1 + 4 * i;
                indexes[4 + 6 * i] = 3 + 4 * i;
                indexes[5 + 6 * i] = 2 + 4 * i;
            }
        }
        if (USE_CUSTOMMESH) {
            mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
            mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord));
            mesh.setBuffer(Type.Index, 3, BufferUtils.createIntBuffer(indexes));
            mesh.updateBound();
            geom = new Geometry("Box", mesh);
            mesh.setBuffer(VertexBuffer.Type.Color, 4, new float[16 * SIZE]);
            colorBuffer = (FloatBuffer) mesh.getBuffer(Color).getData();
            Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");  // create a simple material
            mat.setBoolean("VertexColor", true);
            geom.scale(0.18f);
            geom.move(5, 5, 0);
            geom.setMaterial(mat);                   // set the cube's material
            guiNode.attachChild(geom);
        }
    }
    Geometry geom;

    float cc[] = new float[16];

    public void simpleUpdate(float tpf) {
        for (int i = 0; i < SIZE; i++) {
            allMyColors[i] = ColorRGBA.randomColor();

            if (USE_CUSTOMMESH) {
                colorBuffer.position(i * 16);
                cc[0] = allMyColors[i].r;
                cc[1] = allMyColors[i].g;
                cc[2] = allMyColors[i].b;
                cc[3] = allMyColors[i].a;
                cc[4] = allMyColors[i].r;
                cc[5] = allMyColors[i].g;
                cc[6] = allMyColors[i].b;
                cc[7] = allMyColors[i].a;
                cc[8] = allMyColors[i].r;
                cc[9] = allMyColors[i].g;
                cc[10] = allMyColors[i].b;
                cc[11] = allMyColors[i].a;
                cc[12] = allMyColors[i].r;
                cc[13] = allMyColors[i].g;
                cc[14] = allMyColors[i].b;
                cc[15] = allMyColors[i].a;
                colorBuffer.put(cc, 0, 16);
            }
        }
        mesh.setBuffer(Color, 4, colorBuffer);
    }

}


Sure, read the colors from a texture, and introduce a fbo pass that updates that texture

1 Like

Strange … your code still looks like you create a lot of arrays and pass them to a buffer-creating thing. Now, pspeed said that those short-lived objects are basically free, but …
Why don’t you create a buffer, add float values to it and then give the buffer to the mesh? At least that would save your code from creating arrays and copying the values to the buffer.
And in a similar manner you could just call “getBuffer” and set all values again, wich might save you from creating a new buffer each frame.

Not that it would make your code significantly faster but maybe it’s worth a try (and comparison to how it’s now).

About what zzuegg said I don’t know since I never used “fbo pass”.

Reusing buffers would be faster… probably significantly given the amount of data copying that happens otherwise.

Edit: but note that he’s not recreating the array all the time… just once and reuses it. GC never factors in. But it should be more efficient to just put the values directly into the buffer instead of the 16 element array first.

I think this experiment is done for now, and is probably more interesting to get into the implementation on the MonkeySheet library:

Next step is to tweak the material parameters so that they are taken from the buffer. Something like

And

By the way, since all the quads are part of the same Geometry, this means that they must share the same Material, right?

And that the spritesheet texture is shared between all the quads… so I can’t anymore have an animation that spans multiple spritesheets :angry:

Unless your shader supports multiple textures.

Now I’m trying to send a parameter (position on the spritesheet) to the material via a int buffer, like

mesh.setBuffer(Type.Index, 1, BufferUtils.createIntBuffer(myarray));

how do I bind this buffer with the j3md / vertex shader? I’ve studied the Unshaded.j3md but didn’t find the answer.

Hey Pesegato,
why don’t you just use the texture coordinates?
They give you the x and y position (u and v position) in the spritesheet - and you can calculate the int index by using these x and y values.

Also … “Type.Index” is for connecting the dots in a custom mesh - I don’t believe that this is the correct type of VBO (buffer).

I pass a “position” to the shader, which in turn converts the position into actual texture coordinates.

Now I’d like to put this position into a buffer, and probably I have to choose between the predefined set (i.e. I can’t define a brand new vertex buffer for this purpouse)?

First hit when searching “jME Buffer Type”:

EDIT: Type.Index seems to be the one that you shouldn’t use…

1 Like