Batching doesn't seem to do anything

The BatchManager documentation is pretty confusing. On the one hand, the BatchManager seems to do everything automatically, on the other, you seem to have to do it yourself.

I’m generating a map of blocks that is 23 x 23 x 20. I’ve created a batch group and given each mesh the group id. Each block uses one texture and isStatic is set to true. After the map is generated I call this.app.batcher.markGroupDirty(this.blockBatchGroupId);

With lighting switched off, I’m still getting 7735 draw calls, which is the same number as if I don’t call markGroupDirty.

Is the batch manager actually doing anything? What have I done wrong?

Hi @MikeClarke and welcome,

Not sure what the issue is here, can you try instead of using markGroupDirty() the following:

this.app.batcher.generate([this.blockBatchGroupId]);

Have a look at the example here:
http://playcanvas.github.io/#/graphics/batching-dynamic

This uses dynamic batching, as meshes move, but if they do not, you can also use static batching which is even more performant.

Make sure your blocks use the same materials for the batching to work. If each block has unique material, there will be nothing to batch.

3 Likes

This didn’t make any difference. I’ve noticed a few things. I’ve created a block programmatically with manually created vertices etc. If I put only 3 identical blocks in scene, the profiler says that the scene has got 6 draw calls and is using 6 materials. Each block uses only one material/texture. That material thing is probably why the batching isn’t working, but why isn’t there only 1 material?

The example that you gave creates elements using a render component, but mine are using a model component. Will that make any difference?

Ok I’ve switched over to a render component and there’s no change. The profiler suggests that there is a unique material for every block.

Is there something fundamentally wrong in what I’m doing here? I have a bunch of block types that are either a custom mesh created in code, loaded from an fbx, or a primitive.

I create an object that maps material names to material resources.

I store the custom blocks in an entity variable.

Each time I need to add one of those custom blocks, I take the appropriate entity and clone it, then place it at the right location.

Here’s the code I use to create the most common block:

ReadMap.prototype.createStoneBlockAsset = function() // Create custom box for stone block
{
    const StoneBoxVertices = [...];
    const StoneBoxIndices = [...];
    const StoneBoxNormals = [...];
    const StoneBoxUvs = [...];
    ^^ above values removed for brevity

    // Create mesh
    let stoneBoxMesh = new pc.Mesh(this.app.graphicsDevice);
    stoneBoxMesh.setPositions(StoneBoxVertices);
    stoneBoxMesh.setIndices(StoneBoxIndices);
    stoneBoxMesh.setNormals(StoneBoxNormals);
    stoneBoxMesh.setUvs(0, StoneBoxUvs);
    stoneBoxMesh.update();

    // Create the mesh instance
    let node = new pc.GraphNode();
    let meshInstance = new pc.MeshInstance(node, stoneBoxMesh, this.sceneMaterials.stone1);

    // Add a render compoonent
    let stoneBlockAsset = new pc.Entity(); // Create an Entity
    stoneBlockAsset.addComponent('render', { type: 'asset', batchGroupId: this.blockBatchGroupId });
    stoneBlockAsset.render.meshInstances = [meshInstance];
    stoneBlockAsset.render.castShadows = true;
    stoneBlockAsset.render.receiveShadows = true;
    stoneBlockAsset.render.isStatic = true;
    stoneBlockAsset.render.layers = [this.app.scene.layers.getLayerByName('World').id];
    stoneBlockAsset.render.enabled = true;
    
    return stoneBlockAsset;
};

Then when I add a block to the scene:

ReadMap.prototype.makeAsset = function(x, y, z, assetType, materialObject, isFloorObject)
{
    switch (assetType) {
        case 'stoneblock':
            let newStoneBlock = this.stoneBlockAsset.clone();
            newStoneBlock.setPosition(x, y, z);
            this.app.root.addChild(newStoneBlock);
            return newStoneBlock;

    ----8<---- rest of function cut ----8<----

I’ve tried all sorts and I still can’t get the number of draw calls down. Is there anything i can do? How do I make the project publically viewable for someone to check it?

Hi @MikeClarke,

Go to your project page and click Settings, at the left column uncheck PRIVATE and press SAVE:

1 Like

Ah, it’s already public then.

It’s at https://playcanvas.com/project/821929 if anyone wants to have a look.

I’d suggest to start with the simplest test case / project and go from there. Create a single mesh, single material, add them to a group and batch them. Similar to what the engine example I linked does. That way you will be able to see at what point it stops working.

Just so that you know, this is all sorted now.

I honestly can’t really tell you what was wrong with it. But what I do know is that I replaced all model components with render components and that seemed to resolve the problems.

There is still one questionable thing in the profiler and that is that the number of materials seems to always match the number of draw calls. I don’t know if that’s a problem in the engine or just in the profiler.

2 Likes

That’s interesting, that’s something for us to keep in mind and thanks for reporting back! :thinking:

That sounds correct. For meshes to be batched, they have to share the same material.

Unless you are saying that the number of materials reported doesn’t match the number that you are using?

Unless you are saying that the number of materials reported doesn’t match the number that you are using?

From what I can tell there are two situations that seem erroneous:

  1. Unbatched entities that are marked as isStatic generate a new material for each entity

  2. Batched entities that are restricted by maxAABB generate a new material for each sub-batch.

Have a look here: https://playcanvas.com/editor/scene/1221766

I’ve set up some attributes on the script attached to Root. Launch the scene with the Profiler enabled then see what happens to the number of materials when you change the attributes.

I have new batching problem and it looks like a clear bug in PlayCanvas.

I have a series of map files. One of these files is parsed and the level populated with blocks. There is only ever one scene.

When I create my level during initialization, everything is fine. However, if I instead show a front end, then trigger the scene creation, then tell the batch manager to update, none of the batch groups are drawn. I am currently trying to destroy all existing batch groups, then destroying all of the level’s entities, then recreating the batchGroups, then parsing the new map file.

None of the batchGroups gets deleted…because they all have an internal id of 0 (see attached screenshot).

Surely this is contributory as to why people have been reporting problems with the groups?

Can this be fixed? My project is dead with this issue. It’s just not publishable with the frame rate I get without batching.

The batch group ID is a number which gets incremented each time a new batch group is created. The first batch group has ID of 0, so that by itself is not a bug, it’s a valid ID.

what is the ‘id’ of the group you’re trying to remove?

You can see in the screenshot that there are multiple batchGroups and all of them have an id of zero that I’ve circled in red.

Also in the screenshot, in the playcanvas-stable-dbg.js code where you can see the breakpoint, that code never finds a valid batchGroupId to destroy the group because this._batchIds[i].batchGroupId is always zero. It probably only ever deletes the first one created.

In the screenshot there’s a single batchGroup, and it contains multiple batches. They have the same ID as they are part of the same batch group.

For example, if you have many objects in the scene, some with materialA and some with materialB. You create a single batch group … it will have ID of 0. When you add all these meshes to it, internally when it creates batches, it will create two batches - one for each material, but both batches will have ID of 0 as they are part of the same group.

I create my own groups with this code:

    this.batchIds[this.AssetType.STONE_BLOCK] = this.app.batcher.addGroup("Block",     false, 12).id;
    this.batchIds[this.AssetType.PUSH_BLOCK]  = this.app.batcher.addGroup("PushBlock", false, 23).id;
    this.batchIds[this.AssetType.DOOR]        = this.app.batcher.addGroup("Door",      false, 23).id;
    this.batchIds[this.AssetType.GRASS]       = this.app.batcher.addGroup("Grass",     false, 12).id;
    this.batchIds[this.AssetType.PANEL]       = this.app.batcher.addGroup("Panel",     false, 23).id;
    this.batchIds[this.AssetType.BUTTON]      = this.app.batcher.addGroup("Button",    false, 23).id;
    this.batchIds[this.AssetType.LIFT]        = this.app.batcher.addGroup("Lift",      false, 23).id;
    this.batchIds[this.AssetType.STAIRS]      = this.app.batcher.addGroup("Stair",     false, 23).id;
    this.batchIds[this.AssetType.SUPPORT]     = this.app.batcher.addGroup("Support",   false, 23).id;
    this.batchIds[this.AssetType.PUDDLE]      = this.app.batcher.addGroup("Puddle",    false, 23).id;

Each batch group only ever has one type of object added to it. Every object has one unique material.

it looks like all meshes are part of the batch groups with id 0. Or at least all that were batched.

Do you add your render components to appropriate batch groups? For example by doing something like this:

            const entity = new pc.Entity();
            entity.addComponent("render", {
            meshInstances: [myMeshInstance],
            material: myMaterial,

            batchGroupId: batchGroup.id
        });

Yep:

        case this.AssetType.PUSH_BLOCK:
            let newPushBlock = new pc.Entity();
            newPushBlock.addComponent('render', { type: 'box', material: this.sceneMaterials[assetType], isStatic: true, receiveShadows: true, castShadows: true, batchGroupId: this.batchIds[assetType] });
            newPushBlock.setPosition(x, y, z);
            newPushBlock.tags.add('mapEntity');
            this.mapRoot.addChild(newPushBlock);
            return newPushBlock;

my suggestion would be to build your version of the engine, and add logging to both Batch and BatchGroup class (constructor - log ID and meshinstance array and similar), and perhaps even in some places in BatchManager, to see what is going on.

You can also put breakpoints in those places, but that could be more time consuming to step code each time you try some changes.