Unsatisfied with my billboard foliage implementation performance

Hi there, did some tests with billboard foliage, but getting ~30fps on my craptop rendering nothing but 2400 billboards. Not happy with that at all. I was wondering if maybe there is something I am doing that is sub optimal. Would appreciate it if you experienced people would have a quick look at the code and see if there is anything obviously wrong with it (specifically with regards to frame rate)?

Of course, any tips or criticism welcome.

(grass.png is this: http://www.reinerstilesets.de/3dtextures/billboardgrass0002.png)

public class FoliageTest
        extends SimpleApplication
{
    public static void main(String[] args)
    {
        new FoliageTest().start();
    }

    @Override
    public void simpleInitApp()
    {
        flyCam.setMoveSpeed(5f);

        Texture grassTexture = assetManager.loadTexture("grass.png");
    
        Material mat = FoliageTest.createFoliageBillboardMaterial(grassTexture, assetManager);

        float y = 0;
        float density = 0.5f;
        for (float ix = 0; ix < 20; ix += density){
            for (float iz = 0; iz < 20; iz += density){
                float x = ix + (((FastMath.rand.nextFloat() * 2.0f) - 1.0f) * density);
                float z = iz + (((FastMath.rand.nextFloat() * 2.0f) - 1.0f) * density);

                Node n = FoliageTest.createTriangleBillboardFoliage(mat, x, y, z, 1.2f, 0.9f, 0.25f, 15f);
                n.setLocalRotation(new Quaternion().fromAngles(0, FastMath.rand.nextFloat() * 360f, 0));
                rootNode.attachChild(n);
            }
        }
    }

    public static Material createFoliageBillboardMaterial(Texture texture, AssetManager assetManager)
    {
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setTexture("ColorMap", texture);
        mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
        mat.setFloat("AlphaDiscardThreshold", 0.5f);
        return mat;
    }

    public static Node createTriangleBillboardFoliage(Material mat, float x, float y, float z, float width, float height, float overlapRatio, float xRotationDegrees)
    {
        Node pivot = new Node();

        Mesh mesh;
        Geometry g;

        float triangleHeight = FastMath.sqrt(FastMath.sqr(width) - FastMath.sqr(width / 2.0f));

        mesh = new Quad(width, height);
        g = new Geometry("a", mesh);
        g.setLocalRotation(new Quaternion().fromAngles(xRotationDegrees * FastMath.DEG_TO_RAD * -1f, 60 * FastMath.DEG_TO_RAD, 0));
        g.setLocalTranslation(x + ((width / -2.0f) + (width * overlapRatio)), y, z + ((triangleHeight * (1f / 3f)) + (width * overlapRatio / 2.0f)));
        g.setMaterial(mat);
        g.setQueueBucket(RenderQueue.Bucket.Transparent);
        pivot.attachChild(g);

        mesh = new Quad(width, height);
        g = new Geometry("a", mesh);
        g.setLocalRotation(new Quaternion().fromAngles(xRotationDegrees * FastMath.DEG_TO_RAD, 120f * FastMath.DEG_TO_RAD, 0));
        g.setLocalTranslation(x + ((width / 2.0f) - (width * overlapRatio)), y, z + ((triangleHeight * (1f / 3f)) + (width * overlapRatio / 2.0f)));
        g.setMaterial(mat);
        g.setQueueBucket(RenderQueue.Bucket.Transparent);
        pivot.attachChild(g);

        mesh = new Quad(width, height);
        g = new Geometry("a", mesh);
        g.setLocalRotation(new Quaternion().fromAngles(xRotationDegrees * FastMath.DEG_TO_RAD * -1f, 180f * FastMath.DEG_TO_RAD, 0));
        g.setLocalTranslation(x + (width / 2.0f), y, z + ((triangleHeight * (1f / 3f)) - (width * overlapRatio)));
        g.setMaterial(mat);
        g.setQueueBucket(RenderQueue.Bucket.Transparent);
        pivot.attachChild(g);

        return pivot;
    }
}

So each billboard is its own separate Spatial?

Yeah, that’s never going to work.

For grass, you have to batch your grass together into larger chunks and then billboard in the shader.

Thanks for the quick response.

Yes, each billboard is its own spatial (sounds like that is a bad idea).

I dont expect you to spell it out for me, but could you please point me in a direction on how to implement what you have described? Just a link or a phrase to Google please?

I did it in this project:

You can take it apart and look at the code if you want… or just use it.

1 Like

Thank you, your help is much appreciated.

This is what it looks like:

1 Like

That looks lovely. There are quite a few things in that video I want to learn, so I will be picking that project apart. Thanks.

If you wanto to search on the forums use:
GeometryBatchFactory
Instancing
BatchNode
as startingpoint

Though… note my code uses none of those things as it bypasses the helpers and does the mesh creation manually. You will probably have to also if you want billboarded grass.

Also note that instancing is bad for grass but good for trees.

Been a bit busy, this weekend I got an opportunity to read up about this and do some testing.

Your advise helped a lot. I am still creating all the spatials, 3 per tuft of grass, but I am now attaching them to a BatchNode, and calling BatchNode#batch() after they are all attached. Does this sound reasonable?

As I understand, BatchNode converts all my grass quads to one big spatial, made up of lots of disjoined quads, but all sharing the same material, and renders them as one spatial.

FPS is back up to 60. Probably a lot more but something limits it to 60…

You say instancing is bad for grass, but good for trees. I really would like to understand why. Did some reading, please correct me if I got it wrong, this is what I understand:

Batching solves the problem of GPU not good at rendering lots and lots of spatials. Pass all the info and render all the pieces of grass in one call.

Instancing solves the problem of having to load the same model lots of times, when all that changes is the location and rotation. With a tree, that model has a lot more vertices than the simple quads I am using for the grass.

I can see how probably wouldn’t batching wouldn’t help much for trees, because the number of vertices stays the same, and how instancing for trees would help reduce the number of vertices.

What I don’t understand is, how is instancing bad for grass? As always, appreciate your help.

Instancing lets you have one draw call but the driver still has to setup and call the shader once for each instance. ie: there is still per-object overhead it’s just not as much as for multiple draw calls.

For something with lots of vertexes like trees, this is a nice tradeoff because you save a ton on GPU memory… and a little performance, over separate draw calls of shared mesh data. For 1000 quads, it doesn’t make any sense at all. You run the shaders 1000 times to draw two triangles each. Better to eat a little memory and just draw one 4000 vertex shape.

1 Like

Ah, that explains it perfectly. Thank you for your assistance again.