Looking for any tips on optimizing this scene with thousands of meshes (Parthenon)

To begin with, unfortunately I can’t share a playground link since I don’t want to make my model freely available. I’ll post some screenshots at the end to give an idea about what I’m trying to do and the model but in short it’s a full model of the Parthenon from Ancient Greece.

Mainly, I’m looking for any suggestions on how to best optimise my scene and/or model. This doesn’t only have to be limited to things I can do in Babylon.js to improve performance.

The basics:

  • Model has 4,000 meshes and 4.5M vertices (12.5M indices)
  • Collision is enabled for at least a couple hundred meshes, maybe up to 1,000.
  • Meshes don’t need to be pickable
  • I’m using PBR materials and Screen Space Reflections but not Shadows (yet)
  • Textures/meshes won’t change, scale, rotate etc after initialisation, they’ll be static.
  • The scene uses a FPS universal camera, allowing the player to walk throughout the scene and into the building
  • I’m currently texturing everything in Babylon.js because the model is created in Fusion 360 and my Blender skills are limited. As I’m still iterating the model in F360, I don’t want to invest a lot of time in adding textures etc in Blender if I’ll have to update the model and repeat the process later.
  • I’m currently using two hemispherical lights and two direct lights.
  • Most meshes are using 2k PBR textures

Problem:

  • When the building is in full-view, i.e. all the meshes are in-front of the camera, FPS drops significantly (below 30fps). When I get closer to the building, or enter it, so that some meshes are no longer in the field of view, FPS improves and is generally at around 60fps or higher.

What I’ve tried:

  • ScenePerformancePriority.Intermediate. It definitely helps but I still get noticeable stutter when panning the camera in front of the building so all meshes are in the FOV
  • Compressing the .glb from ~140mb to ~27mb. I can’t see any difference in quality of the meshes and the mesh loads faster but once it’s drawn I doubt the reduction in file size affects performance.
  • Freezing meshes and world matrix
  • Disabling lights and SSR helps a little but not a lot, so the issue seems to be related to the meshes being in the FOV.

Questions

  • Would baking textures help at all, or would that likely only solve performance issues directly related to lighting and shadows?
  • It seems that the number of meshes in the FOV is the main problem, would Occlusion Queries likely help or would there be too much overhead trying to check the occlusion of potentially thousands of meshes? Also, part of the issue with this is that even if I used Occlusion Queries for meshes inside the building, it wouldn’t have an impact when the user is inside the building and all the outside meshes are still being rendered.
  • Otherwise, should I just aim to reduce the number of meshes? I noticed when splitting some meshes into much smaller ones so that they could be individually textured, performance became significantly worse. I went from about 1,000 meshes to 4,000 meshes. The alternative is to revert that change and do the texturing in blender where I can more easily texture individual faces.
  • Would using instances help at all, or again would this only improve loading/drawing times? I would also prefer to avoid this because while I have many identical meshes (e.g. the columns, roof tiles, etc), cloning them and placing them in the correct positions would be a real pain.

Thanks!




Quick thing to try would be LODs: Babylon.js docs

Also how many draw calls is it creating? Anything that doesn’t need to be pickable or change at run time individually could be merged to reduce draw calls. You could do this in Babylon at run time (Babylon.js docs) but it might be better to do that in blender ahead of time.

When stepping back to have the whole building in the FOV, the draw calls maxes out at the total number of meshes (just shy of 4,000).

Thanks for the tip about LODs, hadn’t come across that yet. I’ve tried a quick implementation but it seems to be very slow. This is the snippet I’m running on each of the ~4000 meshes:

mesh.simplify([
        { quality: 0.9, distance: 25 },
        { quality: 0.3, distance: 50 },
    ],
    false,
    BABYLON.SimplificationType.QUADRATIC,
    function () {console.log("optimised mesh");}
);

Looking at the count of the log statements in the console shows it progressing very slowly, like one mesh every 10 seconds or so. Even if I selectively applied it to just the meshes with most faces, I’m sure it’d still take too long. Unless I’m doing something wrong, which is quite possible.

For merging meshes, I assume this would mean their bounding boxes are recalculated so there is now just one bounding box for the merged mesh? So while I could merge the meshes making up a single column (about 4-5 meshes per column), merging all the columns into a single mesh would mean the bounding box would encompass the space between columns?

You don’t need to run it every time if you prepare LODs beforehand (for example, as separate GLB files). It is possible to to with GLTF-Transform CLI, for example.

While your model may need some optimization, the one way to reduce draw calls is instancing. For example, EXT_mesh_gpu_instancing for GLTF - this extension is specfically designed to enable GPU instancing, rendering many copies of a single mesh at once using a small number of draw calls. More info here - glTF/extensions/2.0/Vendor/EXT_mesh_gpu_instancing at main · KhronosGroup/glTF · GitHub

You may try to instance your model with GLTF-Transform CLI instance command and see what will happen :slight_smile:

Try to use just one low-poly invisible collider mesh instead of 1000 meshes.

1 Like

Assuming you need collisions so people cant walk through the walls or columns you could also use a nav mesh as well. Babylon.js docs

Hmm so you would just discard a mesh once you reached a certain distance and draw the prepared LOD version for that distance? That could be an option if so, though I’m sure I’d get some flickering of some sort as the player crosses the distance threshold and hundreds of meshes are redrawn.

Re: instancing, I did think about that but as I briefly mentioned above, it would be quite a pain to place all the clones in their correct locations. In terms of the math I don’t think it’s difficult but it would require a lot of manual effort to record the starting location, offsets, and count for each group of meshes. The model is largely symmetrical along the X and Y axis so that would make it slightly easier but still a lot of work.

Ah I didn’t notice this would create the Navmesh for you! I had looked into when starting out on Babylon.js but at the time it seemed like too much trouble to make your own.

Though, to me, it doesn’t seem like collision detection is the performance bottleneck in my scene, unless the camera’s FOV determines which meshes are assessed for collisions?

Here’s a screen recording of the scene, it’s smoother than it appears here but you can see the points where it really struggles and the frame rate drops: Watch 211334 | Streamable

Using a nav mesh would let you merge all the meshes together into one mesh because you don’t need their individual bounding meshes anymore. If you merged all the meshes you could also still create empty meshes or low polly meshes set to invisible with their bounding box set around the meshes you want to have collisions.

This would make everything one draw call which would improve performance. Instancing would have the same affect and I believe if you do this before hand in your gltf file you wont need to re position everything, but someone can tell me if im wrong about that.

1 Like

Thanks for the tips! I’ll have a look into navmeshes and whether I can do instancing in the gltf too, I didn’t realise that was a possibility before.

I’m now wondering whether Blender’s gltf compression is doing something similar since it managed to reduce the texture-less .glb file so much without an apparent decrease in mesh quality as far as I could tell (the vertices count did increase slightly though).

At any rate, I’ll have a go with a few of these ideas this week when I get some more time.

Just to provide a quick update for others who might stumble upon this thread.

I ended up doing the texturing in Blender, which was actually quite a bit easier than I expected (much better selection tools than Fusion 360 helps). That allowed me to join a lot of meshes together. Rather than worry about a Navmesh just yet, I mainly joined together meshes that wouldn’t have collisions enabled.

I managed to get the active mesh count down to under 800 from nearly 4000 and that has helped immensely with performance. I’m now at over 100fps with the whole building in the FOV and maxing out at 144fps when inside the building and walking around.

I’ll finish the texturing work (see if I can bake in the lighting) and do a couple of other things and then check performance on some other devices. If I need to get a bit more out of it, I can try some of the other suggestions given above.

3 Likes