Performance issue due to Fill rate

Hey All,

I am working on a threejs project where I need to render multiple objects(glTF models) and also have a continuously moving camera.
{geometries: 137, textures: 17}

Initially, there were around 5500-6500 draw calls happening and after tweaking the models a bit, I was able to bring it down to ~2000 draw calls. However, this did not seems to improve the FPS noticeably (still around 20 FPS).

As I was looking for a reason for this, I found this SO answer which talked about the fill rate. On trying this approach of debugging, I did find that “Fill Rate” was the one making the FPS reduce. My project was running at 55-60 FPS on smaller window size.

Also, it is very weird that just adding the below causes the FPS to reduce by ~10 frames. (With floor 20FPS and without floor - 30FPS)

var floorTexture = textureLoader.load( "/assets/models/textures/antiStaticFloor.jpg" );
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
floorTexture.repeat.set( 1024, 1024 );
var floorMaterial = new THREE.MeshPhysicalMaterial({ color:0xe8f1ff, reflectivity:0, map: floorTexture })
var floorGeometry = new THREE.PlaneBufferGeometry( 500 , 500, 50, 50 );
var floor = new THREE.Mesh(floorGeometry,floorMaterial);
floor.rotation.x -= Math.PI/2;
floor.receiveShadow = true;
scene.add( floor );

I may be making a mistake somewhere or really bad with texture mapping in the above case but unable to find the reason.

Also, really appreciate any direction regarding the Fill Rate as I am unable to find a solution to handle it.

Does it have to be handled via custom shaders or is there a way to control them from threejs?

Best Regards,

Helping with performance issues without debugging the application is always hard. But still here are my two cents:

  • If you lowering the resolution (by decreasing the size of the browser window) and you see no performance improvement, then the application is not fragment shader bound.
  • 2000 draw calls is still quite a lot. Ideally you have something like 20. I assume the respective CPU overhead kills the performance in your app right now.

Thanks @Mugen87 for the two pointers,

I probably didn’t frame the question in the sequential order.

By decreasing the size of the browser window, the performance improves greatly (from 20FPS to 55FPS). That is the reason for posting the question on how to handle the Fill Rate. I thought if I could do something about the fill rate, I could get away with the draw calls and the CPU overhead it may cause by finding a balance among the two.

Regarding the draw call, I am stuck in a weird situation.

  • I cannot merge the geometries as I need to access them individually again.
  • I have only 15 unique glTF models but have cloned them multiple times as I am not able to instance the glTF as each glTF has multiple meshes and materials :frowning_face:

The only way I am trying now to address it is to further reduce the individual mesh count to a minimum I could accommodate without compromising the model features.

Is there any other direction I should look at?

Have you tried to use instanced rendering? It’s a bit more complicated than just merging geometries since you have to adjust shader code. However, it’s easier to modify individual entities with this approach.

Having a good visual quality and proper performance is the big challenge in computer graphics. However, there is no way around simplification in your case. It’s already important in the design phase to regard performance. For example try to represent a character model with one mesh and a single material if possible.

Like I said earlier, our glTF model has multiple meshes and geometry and hence not able to figure out instanced rendering as such.

Moreover, the 2000 draw calls work smoothly at 60FPS if I reduce my browser window size to 1/3 of full screen, so I believe it is something to do with FILL RATE, but I could be wrong.

I am still looking at the web for a way around this issue. If anyone else has any other approach, it will be highly appreciated.

As another test, what happens if you keep the resolution at full size, but move the camera far enough away that the models cover only a small portion of the screen? Are the objects still visible, in all of these tests, to be sure they aren’t culled?

I agree with @Mugen87 that fill rate is unlikely to be a significant problem compared to 2,000 draw calls, but if you do want to make the fragment shader cheaper, try replacing all of the materials before rendering. For example:

var material = new THREE.MeshBasicMaterial();
scene.traverse((o) => {
  if (o.isMesh) o.material = material;

MeshStandardMaterial has a more complex fragment shader than MeshBasicMaterial.

1 Like

Thanks @donmccurdy, Please find the below observations.

This test gets me a very good result. I was able to get around 55-60FPS constantly. I made sure none of the objects were culled and made sure the draw calls are the same as well.

There is a catch in this as some of the material needs to cast/receive shadows whereas MeshBasicMaterial would not.
I know rendering shadows is expensive so I do a one-time rendering of shadows at the start and once the scene is loaded, I set the below,

renderer.shadowMap.autoUpdate = false;

Just curious between the draw calls and fill rate concept - If the draw calls affect the rendering FPS, how does it renders at a much higher FPS when the camera is moved away/ window size/resolution is reduced? This looks really interesting.

Ignoring the shadows for a second, though, does MeshBasicMaterial (without shadows) give you better performance than MeshStandardMaterial (without shadows) in this case?

If the draw calls affect the rendering FPS, how does it renders at a much higher FPS when the camera is moved away/ window size/resolution is reduced?

Draw calls always affect FPS, but no, we don’t know for sure that it’s the primary cause of the low framerate in your app and on your device. A demo can save a lot of guessing in a case like this. :innocent:

Thanks again @donmccurdy for your quick replies! :slight_smile:

It improves by just 5FPS (from 25FPS to 30FPS) and not more.

Apologies, this project actually comes with an NDA(Non-Disclosure Agreement) so I would not be able to provide a demo with the exact models :frowning_face: :frowning_face:

You can also use scene.overrideMaterial for this.

This is very true and turns out to be the cause in my case. Having 2000 draw calls did tamper the performance.

I did the below to reduce the number of draw calls:

  1. Reduced the number of geometries and the materials in the Models to 1 and used textures for detailing which was a fair trade-off.
  2. Instanced meshes that were repeated in the scene (This was only possible after completing the first point)

Reducing the Draw calls gave some room for the Fill rate and both were compensating each other creating a balance and improvement in the performance.

Many thanks to @Mugen87 and @donmccurdy for the assistance in this :slight_smile: