Mobile optimization - draw calls and material offset and tiling

I’m trying to optimize the scene of my game, for older mobiles.

I have a number of 3D objects in my scene ( cards ), and am now trying to share a textured material between them, in order to reduce the draw calls.

Each material has a different offset and tiling into the texture.

As it stands, the draw calls are around 40;

If I batch group the meshes, and assign the same material to all the objects ( same offset, same tiling ), I see the draw calls drop a lot - to 13.

    var vec4 = new pc.Vec4(1, 1, 0, 0);  //tileX, tileY, offsetX, offsetY
    model.meshInstances[0].setParameter('texture_diffuseMapTransform', vec4.data);

If I then try to change either the tiling or offset for each object, the draw calls revert back up to 40.

    var vec4 = new pc.Vec4(Math.random(), 1, 0, 0);  //tileX, tileY, offsetX, offsetY
    model.meshInstances[0].setParameter('texture_diffuseMapTransform', vec4.data);

So - should it be possible to have different meshes use the same material but with different offsets and tilings, and get drawn with a single draw call?

It’s like using a texture atlas, but on 3D models rather than sprites.

Hi @Mal_Duffin,

Good thinking in using batching for this. Sadly one of the constraints with batching is:

  • All mesh instances should be using the same material/shader uniforms to belong to the same batch group.

If all 40 models use a different diffuseMapTransform then that’s the best you can get, as far as draw calls are concerned.

1 Like

should it be possible to have different meshes use the same material but with different offsets and tilings, and get drawn with a single draw call?

I went and set up the same scenario, where the same different meshes had the same material but with different offsets and tilings, but in this case I directly set this in Blender ( using the UV editor ), and I was able to get the lower draw calls, down to what I had seen before.

As map transforms only have an effect on the UVs, should batching allow for this exception?

The way batching works is by merging all meshes instances of a batching group to a single mesh instance using a single material. That’s 1 draw call.

At that point all individuality has been lost, so there isn’t any way to target part of that meshInstance and apply different uniforms.

1 Like

An option here might be to “bake” the UV coordinates before merging the meshes, but setting the UVs manually works great for now.

1 Like

After reading this thread, I decided to go and check how my game is doing draw call wise. And it was doing terrible :slight_smile: Well, not too terrible, but if we consider 140-160 draw calls for high end mobile devices and 40-60 for low end, then I am no way near low end spectrum with my ~150-ish. So, hereby, kind sirs, I hope you could share your wisdom with suggestions on what could bring the draw calls down.

My game home screen is probably the most demanding power wise, as it shows all the game models (22 static, excluding floor, 1 dynamic, 12 materials):
image

My first thought was to simplify shadows or disable them completely. I might consider faking them with simple round textures, if a “low graphics” setting is chosen from the in-game menu. That did bring it down considerably:
image

Next thing I tried, was to add all those static models into a batch group by assigning them a batch group id:

// pseudo code
var batchGroup = this.app.batcher.addGroup('Platforms', true, 100);
templates.forEach(function(template) {
    var platform = template.clone();
    platform.model.batchGroupId = batchGroup.id;
    root.addChild(platform);
});

The result was a slight increase in other commands, related to batching I suppose, but no change to draw calls:
image
I tried to make the batch group static, but it made no change. Plus, all my models become static in the game, which is a “no go”. Perhaps I am not batching them correctly?
image

So, I am out of options, or it might be the limit. I could potentially explore an option to reduce a number of materials from 12 to something less. Should probably help. Any advice would be appreciated :slight_smile:

Hi @LeXXik,

If you are only using 12 materials in your scene it should be pretty easy to get to < 50 draw calls. You just need to instruct the pc.Batcher on what to run on and when to run.

  1. Prepare your batch groups. Take a look at the manual page regarding batching to understand how to bet group your objects to really take advantage of what the batcher does:

https://developer.playcanvas.com/en/user-manual/optimization/batching/

The basic idea is that your grouped objects can use different models but they must use exactly the same material/materials/shader params.

So if you have a table, a chair and a closet all using the same material/materials they can be in the same batch group. Create a batch group and assign it to all those model components.

  1. Now the question of static vs dynamic batching:
  • If you are setting your scene once and never moving your objects you should go for static.
  • If your objects need to move at some point, you should be using dynamic (unless you are OK with the time it takes to rerun the batcher to regenerate static batches).

In your case, I would go with dynamic batching and pre-creating in pools all objects ahead of play time (even for the starting page, that could be the objects pool).

After all pools have been created (if you are doing this on runtime) make sure to rerun the batcher to generate the total batches:

this.app.batcher.generate();

If you are creating all of your entities in the editor, there is no need to run that, the engine runs it automatically ahead of the first rendered frame.

Each time a new object is required move it from the pool in place, when it’s no longer required move it out of the camera view.

PS Just in case you aren’t aware: on your launch window you have a profiler available using ALT + T, it’s quite handy to inspect many performance related stats.

4 Likes

Thank you for the advice @Leonidas!
I went on and followed the guide you referred to create batch groups. This also gave me an opportunity to optimize my models, e.g. removing, adding or switching materials, which would allow to assign a model to one or another batch group.

By creating, I would assume you mean adding the models into the game as assets? I do add them through the editor, initially disabled. Then, at runtime, I grab one, clone it and show. I guess I don’t need to run batcher.generate() in this case.

Hmm, I didn’t get this one, actually. In the Editor, I have an entity, which holds all the templates of models. They are all disabled at start.
If a home screen is shown, all those templates are cloned and shown in a composition. Now, after your advice, I created 8 groups in the Editor and assigned all models into appropriate group, each sharing the same set of materials. Is this enough? Or do I need to add cloned entities to batch groups after cloning?
When I start the game, I destroy all the clones (entity.destroy()) and start the game sequence, cloning and showing new templates when necessary. Do you mean that here, I should not destroy them, but instead simply teleport in and out of the camera? I thought that the game asset is still loaded in the game, so I can destroy entities safely here.

I couldn’t decipher this one :frowning: What is a pool of objects and how do I pre-create it? Assuming a batch group is a group of similar entities, do you mean a pool is a group of batch groups?

PS
Ah, nice, thank you for the ALT+T tip! I didn’t use it, because it was covering half of my screen and I couldn’t use the game menus as a result. With this hotkey, it is much simpler now :slight_smile:

PPS
Still a few open questions left, but already got sub 60 together with shadows!

1 Like

Ok, so before rendering the first frame the Playcanvas app will automatically call the following method:

this.app.batcher.generate();

It will attempt to find entities with a model component, in the scene hierarchy, that belong to a batch group. And it will generate the batches, ultimately disabling those mesh instances, rendering in place the batched geometry.

Now if you are cloning entities in your code that have a model component assigned to a batch group you will have to call again the above method, so you generate batches for those as well. A good strategy is to do that as the last step, after finishing with all of your cloning, run the batcher.

Of course if you are cloning 1-2 entities every now and then, running the batcher like that will have a performance impact. Here comes the concept of pooling.

It’s a common practice to avoid expensive runtime create/destroy object calls that make can make the garbage collector struggle to keep up.

With pooling instead of creating/destroying objects when you need them, you pre-create a number of entities before your game main loop and keep them hidden/disabled.

  • When you require an object instead of cloning/creating a new instance, you grab one from your list and enable/position it in place.
  • When you no longer need it instead of deleting it, you disable it and hide it out of view.

Apart from the advantage of avoiding expensive garbage collection calls, you can leverage dynamic batching that way. As soon as you finish creating your pool, run the batcher.

Now all of your entities are batched and you can position them in place as you like (make sure to use dynamic batch groups).

1 Like

Playcanvas has an undocumented helpful class for that, used internally by the engine but you can easily use it in your code as well: