Optimizing a Large-Scale Babylon.js Scene

Joe Pavitt
10 min readOct 19, 2021

This article is part of a series of articles about the Digital Experience for the Mayflower Autonomous Ship.

Behind-the-scenes view of the Harbour scene, showing edges and faces within our product-centric buildings. Brighter areas show higher density of edges, and flag potentially non-optimised models.

In this piece, we will focus on the optimisation and architectural techniques used in order to optimise the Babylon.js harbour scene. In total, our scene had over 600 meshes and 1,000,000 vertices. It ran consistently at 45+ FPS in Google Chrome on our 2018 Macbook Pro. Firefox we found to be about 40 FPS, and Safari much lower, although still useable, at 25 FPS, mostly because it does not support WebGL 2.0.

Optimisation techniques discussed here do cover Babylon.js, but are also focussed on methods to improve the underlying models, materials and lighting that we use which has a huge impact on performance. All of our models were built using Blender, and as such, examples included here often reference Blender solutions, but any other 3d computer graphics software can be used.

Note: There are many more optimisation techniques available than described here, and some may not be applicable to your own environments (e.g. some of our techniques required our models to be static in the scene). For a larger list of recommended techniques checkout Babylon’s own article on the matter.

Single vs Multiple Model Imports

There is a trade-off required when designing the assets for your scenes. Let’s take our Harbour as an example.

Birds-eye view of the 3d “Harbour” model used in the Mayflower Digital Experience.

We could model the entire scene in a single .blend file and transfer it to Babylon via single .gltf file. Alternatively, we could design each building, boat, tree and rock in standalone .gltf files and then import, and position, the assets within the Babylon.js scripting.

Fewer models and meshes mean that the Babylon render loop has less to cycle through. Arranging a scene in 3d software is also easier, and can be done by members of the team that are not skilled JavaScript programmers. However, load times can be significant if you have a particularly large model (for context, our final .glb file was 8MB).

Alternatively, we could build our individual assets (buildings, trees, etc.) in their own .gltf files and then import these files and use JavaScript to position them. This is less practical for designing layouts and spaces than in your own 3d software but is rewarded in loading times as models can be loaded in parallel. It would also make it easy to utilise Babylon’s own instancing functionality leading to further optimisation in the sake of tree placement for example.

In the MAS Digital Experience we settled for a mixture. Mostly, we relied on the assets being constructed outside of Babylon.js and in Blender instead. We found having that freedom for non-developers to edit the scene far too useful to sacrifice. We were satisfied with the loading times as the experience was always pre-loaded with an introductory animation and so the scene’s loading, taking place in the background, was less apparent. Additionally, we knew we would want to take high-quality renders of our scenes in Blender. Having the full scene constructed there meant that the static renders reflected the browser-based world perfectly.

Monitoring Performance with Babylon.js

In order for us to optimise, we need to be conscious of our performance throughout development. Whilst Babylon.js has its own excellent Inspector for debugging, the simplest resource we found useful in development was creating our own FPS counter that stayed in the corner throughout. The value for this can be exposed by the Babylon engine.

View of the MAS Harbour scene, showing the small, custom-built FPS counter in the top-left corner.

As we added new resources to our scenes, it became very clear when we were doing something detrimental to performance. Additionally, when consciously optimising our scenes, this gave a very easy-to-read indicator of improvements.

Be sure to interact with your scene when doing this though. Just because your scene runs at 60 FPS when the camera is static, doesn’t mean it’ll stay that way. Move around, click things, do things your user will do!

If you’re struggling to find where the performance impact is taking place, we found these really useful Babylon features in the Inspector that helped:

Wireframe & Point Rendering

In Babylon’s Inspector we have the option to change our rendering mode. You can view the Inspector by including the following line in your scene’s code:

scene.debugLayer.show()

From the menus that now show, click on the “Scene” object within the “Scene Explorer” on the left. On the right-hand side, under “Rendering Mode”, you can then toggle between “Point”, “Wireframe” and “Solid”. These alternative views we found gave a very clear image of polygon density.

Screenshot of Babylon.js’ Inspector view, where it is possible to switch between “Point”, “Wireframe” and “Solid” rendering modes.

In this case we could see that the building in the centre of this image, representing IBM’s Operational Decision Manager, was still poorly optimised given how many edges, and consequently, polygons it contained.

Show/Hide Mesh & isEnabled Toggle

Another of the Inspectors most useful features was the ability to toggle on/off each mesh in your scene. This enables a really easy way to monitor the impact of specific resources on your scene performance, that may not be linked to poly count.

There are two options on how to do this:

  1. The “eye” icon to the right of any Mesh in the “Scene Explorer”.
  2. The “IsEnabled” toggle for a Babylon TransformNode. This option shows in the “General” tab of the Inspector on the right, after clicking on a TransformNode in the Scene Explorer.

In Blender, we found that parenting groups of objects to a single Empty created Transform Nodes. These were especially useful here, as, when imported into Babylon, it was possible to toggle the “isEnabled” option on/off each node, and consequently all children in one hit, making the hunt for potential performance improvements much easier. Blender collections were lost on export to gltf.

With our full scene enabled, we were achieving 48 FPS.

With product buildings toggled off, a gain of 1 FPS was achieved.

When toggling off the ‘isEnabled’ option in the Inspector for our product-related buildings, the impact on FPS was minor — now at 49 FPS.

Filler buildings being toggled off provided an increase of 11 FPS, up to 59 FPS.

When toggling our “filler” buildings off though, we noticed a significant improvement, with the frame rate jumping up to 59/60 FPS. Clearly, these meshes were where we needed to focus our attention and optimise further.

Standard 3D Optimisation Techniques

There are standard good practices that we should follow here that are applicable across any real-time rendered scenes, whether that be Babylon.js and browser-based, or not.

Poly Count

The simplest metric to account for is poly count. It’s much quicker to render 4 vertices than it is 4,000,000. Where possible, be smart with your models and only add detail where it matters. There is no point sculpting a high definition face on a character model if your camera is so far zoomed out that you will never see it.

Ian Hubert’s 1 minute “lazy” Blender tutorials are a great example of how much “detail” can be attained, even with super low-poly meshes. It’s great to build high definition models for 4k renders and top-budget movies, but if you’re looking to build a browser-based experience, then be flexible in where, and how, your models have detail.

A “before and after” view of the retopology of a rock model taken from SketchFab’s guide on retopology of 3d scanned assets

If you have a high-poly model and are looking to optimise it, then retopology is your friend. It can be a tedious process, but the performance improvements are significant.

If you have buildings with details like windows or doors, then you can also bake normal maps to give the appearance of detail, without the penalty in vertex count. Normal maps are especially effective at adding high detail without sacrificing polycount.

left: the model with an applied normal map; middle: the base mesh without a normal map; right: the normal map. Source: http://wiki.polycount.com/wiki/Normal_map

It is worth noting that .gltf meshes have triangulated faces, so don’t be surprised when you see a large “face count” jump when you move your model into Babylon. Where possible, triangulate faces before the import to ensure that you have control over the polygon and face counts.

Instancing

As mentioned above, it’s quicker to render 4 vertices, than 4,000,000. A way that we can “trick” our renderer into thinking we are using less vertices is through Instancing. This reduces the total number of draw calls that Babylon has to go through in each render.

In our finished Harbour scene, we had 631 active meshes, with 73 draw calls. Without instancing, we would expect 631 draw calls, and for rendering to be 5–10x slower in our case.

Instancing: Highlighted instances of a single building within Blender.

Instances are an excellent way to use hardware accelerated rendering to draw a huge number of identical meshes (let’s imagine a forest or an army). This can either be done within the 3d software of your choice (we use Blender, where it can can be achieved using a linked duplicate), or within Babylon.js directly should your scene suit the use case where you are positioning objects from within your JavaScript.

One negative consequence of instancing and re-using the same model multiple times within a scene is that it can often be obvious that you have rinsed and repeated the same mesh over and over.

Here we share some example renders from Blender showing how to utilise instancing whilst keeping a sense of scale and variability in your models.

left: 16 vertices (with instancing); 16k vertices (without) — middle: 48 vertices (with); 48k vertices (without) — right: 228 vertices (with); 228k (without)

In our first image, we have a single building instanced 1,000 times. With the second render, we use 3 material variations of the building split equally across the 1,000 instances. Finally, in the third image, we modify the buildings such that they are not symmetrical, and introduce random rotation about the vertical axis. These small changes introduce a sense of variability, whilst maintaining the optimised geometry.

Extra Consideration for WebGL — Material Consolidation

When working with Babylon, files are generally imported using .gltf format. You may notice though, that when your models are loaded into Babylon, that it has split your model into multiple, separate objects.

This occurs on a material-by-material basis and means that in the render loop, Babylon is looping through each newly created object each time, rather than just the single, original, mesh — expensive!

The ideal fix here is that you have as few materials as possible, and whilst you may want every object to have beautiful 4K textures and PBR materials, if you’re able to, and it fits your desired aesthetic, you can use a Colour Palette.

Left: Diverse colour palette created from the IBM Carbon Palette. Right: The palette we used for our harbour scene, which includes gradient segments.

With your colour palette, you can define a single material for all of your objects. Set the base/albedo colour of that material to the colour palette texture, and ensure the UV faces of your model line up to the colour you desire.

To do this, in your 3d software, UV unwrap the object, scale the object’s UV down to nothing, and then move the UV into the coloured square matching the colour you want that face to be. If you’re using Blender, Imphenzia does this regularly in his great 10 minute Blender challenges, he goes into detail on this technique here.

Each colour palette only needs to be, say 3x3 pixels (for 9 different colours), or 8x8 pixels (for 64 colours). You can also go for a larger palette which gives you the option to add gradients into your palette too. We used this technique for the harbour’s colour palette in the Mayflower Experience as shown above.

Babylon.js Tricks & Techniques

As mentioned previously, Babylon’s documentation is rich with methods to tackle memory usage and performance issues. Two particularly useful resources we found to be:

Specifically, we used the following methods across our scenes and mesh importing where appropriate:

Scene: Disable object interaction

If, in your scene, you do not need to directly interact with the 3d meshes, then disabling the mouse events we found very effective.

scene.pointerMovePredicate = () => false;
scene.pointerDownPredicate = () => false;
scene.pointerUpPredicate = () => false;

This method was used in our Harbour and Challenge 3 scenes. We could not use this in Challenge 1 given that we needed to detect users clicking the environmental objects (rocks, buoys & ships).

Scene: Removing cached vertex data

All vertex buffers keep a copy of their data on CPU memory to support collisions, picking, geometry edition or physics. If you don’t need to use these features, you can call this function to free associated memory:

scene.clearCachedVertexData();

Mesh Import: Static Assets

If you have meshes that will not change position, rotation or size, then it becomes very effective to “freeze” the mesh by calling:

mesh.freezeWorldMatrix();

Even if they do change, but intermittently, then you can toggle the world matrix calculation back on by calling:

mesh.unfreezeWorldMatrix();

Mesh Import: No Interaction

If you have no requirement for users to click or pick your meshes, then the following lines can be added when importing your mesh for further performance enhancements:

mesh.isPickable = false;
mesh.doNotSyncBoundingInfo = true;

VueJS & Babylon

After our project had finished, we actually discovered a gotcha in using BabylonJS and VueJS. Whilst we were very happy with our FPS/performance here, it turns out we were causing a huge FPS deficit by binding the BabylonJS engine and scene as reactive variables in VueJS.

I won’t go into too much detail here, but it is covered in excellent detail in this forum post.

Concluding Thoughts

Thank you for reading through this article. I hope that at least one point you’ve learned here can help you optimise your large-scale Babylon.js scenes.

--

--

Joe Pavitt

AI, UI, Data Visualisation & 3D Modelling. Master Inventor & Senior Research Engineer @ IBM Research UK. MEng Aerospace Engineering w/ Spacecraft Engineering.