BabylonJS Large Scene Optimization Case

In this article, we'll focus on the optimization and architectural techniques used to optimize the Babylon.js port scenario.

In total our scene has over 600 meshes and 1,000,000 vertices. It consistently runs at 45+ FPS in Google Chrome on our 2018 Macbook Pro. We found Firefox to be around 40 FPS and Safari to be at a much lower but still usable 25 FPS, mostly because it doesn't support WebGL 2.0.

insert image description here

Recommendation: Use NSDT Designer to quickly build programmable 3D scenes.

The optimization techniques discussed here do cover Babylon.js, but also focus on ways to improve the underlying models, materials and lighting we use, which has a huge impact on performance. All of our models are built using Blender, so the examples included here generally refer to Blender solutions, but any other 3D computer graphics software can also be used.

Note: There are many more optimization techniques than described here, and some may not be applicable in your own environment (for example, some of our techniques require our models to be static in the scene). For a more list of recommended techniques, check out Babylon's own article on the subject.

1. Single model and multiple model import

There are trade-offs when designing assets for a scene. Take our port as an example.
insert image description here

An aerial view of the 3D "port" model used in the Mayflower digital experience

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

Fewer models and meshes means fewer loops in Babylon's render loop. Arranging scenes in 3D software is also easier and can be done by team members who are not skilled JavaScript programmers. However, if you have a particularly large model, loading times can be lengthy (for context, our final .glb file was 8MB).

Alternatively, we can build individual assets (buildings, trees, etc.) in their own .gltf files, then import those files and use JavaScript to position them. This is less practical for designing layouts and spaces than with your own 3D software, but allows for faster load times as models can be loaded in parallel. It can also easily take advantage of Babylon's own instancing capabilities to further optimize tree placement etc.

In the MAS digital experience, we chose a hybrid approach. Most of the time we build assets outside of Babylon.js, ie in Blender. We found the freedom to let non-developers edit the scene very useful and not to be sacrificed. We're happy with the load times, as the experience always preloads the intro animation, so the scene loading going on in the background is less noticeable. Also, we knew we wanted a high-quality render of the scene in Blender. Building the full scene there means the static rendering perfectly mirrors the browser-based world.

2. Babylon.js performance monitoring

In order to optimize, we need to be aware of our performance throughout the development process. While Babylon.js has its own excellent debug inspector, the easiest resource we've found useful in development is to create our own FPS counter that's always left in the corner. The Babylon engine can reveal its value.

insert image description here

MAS port scene view, showing small custom FPS counter in upper left corner
When we add new assets to the scene, it becomes very obvious when we do something that hurts performance. Also, this provides a very easy to read indicator of improvement when consciously optimizing our scene.

Be sure to interact with your scene when doing this though. Just because a scene runs at 60 FPS when the camera is still doesn't mean it will stay that way. Walk around, click on things, do what your users would do!

If you're struggling to figure out where the performance impact is happening, we've found these very useful Babylon features in the inspector that can help you:

2.1 Wireframe and Point Rendering

In Babylon.js' inspector we have the option to change the render mode. The inspector can be seen by including the following line in the scene code:

scene.debugLayer.show()

From the menu that now appears, click on the Scene object in the Scene Explorer on the left. Under Render Mode on the right, you can switch between Points, Wireframe, and Solid. These alternative views we found give very sharp polygon density images.

insert image description here

Screenshot of the inspector view of Babylon.js, where you can switch between Point, Wireframe, and Solid rendering modes
In this example, we can see that the building in the center of the image (representing the IBM Operational Decision Manager ) is still poorly optimized considering how many edges and thus polygons it contains.

2.2 Show/hide grid and isEnabled toggle

Another most useful feature of the inspector is the ability to toggle on/off each mesh in the scene. This provides a very easy way to monitor the impact of a particular asset on scene performance, possibly independent of polygon count.

There are two options on how to do this:

  • The "eye" icon to the right of any mesh in Scene Explorer.
  • "IsEnabled" toggle for the Babylon TransformNode. This option appears in the General tab of the Inspector on the right after clicking the transform node in Scene Explorer.

In Blender, we found that parenting an object group to a single empty created transform node. These are particularly useful here, as when importing into Babylon, each node's "isEnabled" option can be toggled on/off, thus making it easier to find potential performance improvements with one click of all child nodes. Blender assemblies are lost when exporting to gltf.

With full scenes enabled, we hit 48 FPS.

insert image description here

Gained a 1 FPS gain after closing a prod build

When turning off the "isEnabled" option in the inspector for buildings related to the product, there is very little impact on FPS - now 49 FPS.
insert image description here

Turning off infill buildings increased 11 FPS, up to a maximum of 59 FPS.
However, when turning off "infill" buildings, we noticed a significant improvement, with framerates jumping to 59/60 FPS. Obviously, these meshes are where we need to focus our attention and optimize further.

3. Standard 3D optimization technology

There are some standard good practices we should follow that apply to any real-time rendered scenario, whether Babylon.js or browser-based.

3.1 Polygon count

The simplest measure is polygon count. Rendering 4 vertices is much faster than rendering 4,000,000 vertices. Where possible, work with your models subtly, adding detail only where it matters. There's no point in sculpting HD faces on character models if your camera is zoomed out so far that you can never see it.

Ian Hubert's 1 minute "lazy" Blender tutorial is a good example of how much "detail" you can get even with ultra low poly meshes. Building HD models for 4k renders and high-budget movies is great, but if you want to build browser-based experiences, you need the flexibility to choose where and how details are rendered in your models.

insert image description here

A "before and after" view of a rock model retopology, taken from SketchFab's 3D Scanned Asset Retopology Guide
If you have a high poly model and want to optimize it, retopology is your friend. This can be a tedious process, but the performance improvement is significant.

If your building has details like doors and windows, you can also bake normal maps to give the appearance of details without reducing the vertex count. Normal maps are particularly effective at adding high detail without sacrificing polygon count.

insert image description here

Left: model with normal map applied; middle: base mesh without normal map; right: normal map

It's worth noting that .gltf meshes have triangular faces, so don't be surprised to see a huge increase in "face count" when you move your model to Babylon. If possible, triangulate the faces before importing to ensure you can control the polygon and face count.

3.2 Instantiation

As mentioned above, rendering 4 vertices is faster than rendering 4,000,000 vertices. One way we can "trick" the renderer into thinking we're using fewer vertices is through instancing. This reduces the total number of draw calls that Babylon must perform in each render.

In our finished harbor scene, we have 631 active meshes, and 73 draw calls. Without instancing, we expect 631 draw calls and render 5-10 times slower in our example.

insert image description here

Instancing: Highlighted instance of a single building in Blender

Instancing is a great way to draw lots of the same mesh (let's imagine a forest or an army) using hardware accelerated rendering. This can be done in the 3D software of your choice (we use Blender, use the linked copy for that), or directly in Babylon.js with JavaScript (if your scene suits the use case of positioning objects from within).

A negative consequence of instantiating and reusing the same model multiple times in a scene is that it is often obvious that you have flushed and repeated the same mesh over and over again.

Here we share some rendering examples in Blender showing how to take advantage of instancing while maintaining the sense of scale and variability of your models.

insert image description here

Left: 16 vertices (with instances); 16k vertices (none) — middle: 48 vertices (yes); 48k vertices (none) — right: 228 vertices (yes); 228k (none)

In our first image, we have a building instantiated 1,000 times. In the second render, we used 3 material variants of the building, divided equally among 1,000 instances. Finally, in the third image, we modify the building to make it asymmetrical and introduce a random rotation around the vertical axis. These small changes bring a sense of variety while maintaining an optimized geometry.

4. Additional considerations for WebGL - material integration

When using Babylon, files are usually imported using the .gltf format. But you may notice that when your model is loaded into Babylon, it has split your model into individual objects.

This happens on a material-by-material basis, meaning that in the render loop, Babylon will cycle through every newly created object every time, not just a single raw mesh - expensive!

The ideal solution here is to use as few materials as possible, although you might want to have nice 4K textures and PBR materials for each object, if you can and it fits your desired aesthetic, use a palette.

insert image description here

Left: A diverse palette created from the IBM Carbon Palette. Right: The palette we used for the harbor scene, including the gradient segments

Using palettes, you can define a single material for all objects. Set the base/albedo color of that material to the palette texture and make sure the UV side of the model is the same color as you want.

To do this, in your 3D software, UV unwrap the object, scale the object's UVs to zero, then move the UVs into colored squares that match the color you want the face to be. If you use Blender, Imphenzia does this a lot in his 10-minute Blender challenges, and he details the technique here.

Each palette needs only to be 3x3 pixels (9 different colors) or 8x8 pixels (64 colors). You can also choose a larger color palette, which gives you the option to add gradients to the palette as well. We used this technique on the Mayflower Experience for the color palette of the port, shown above.

5. Babylon.js tips and techniques

As mentioned earlier, Babylon's documentation contains a wealth of methods for troubleshooting memory usage and performance issues. Two resources we've found particularly useful are:

  • Optimize your scene: link
  • Reduced memory footprint: link

Specifically, we used the following methods in the appropriate scene and mesh imports:

5.1 Scenario: Disabling Object Interaction

If in your scene you don't need to interact directly with the 3D mesh, then disabling mouse events we've found to be very effective.

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

We used this approach in the Harbor and Challenge 3 scenes. Since we need to detect when the user clicks on environmental objects (rocks, buoys, and boats), we can't use this feature in Challenge 1.

5.2 Scenario: Delete cached vertex data

All vertex buffers keep a copy of their data on CPU memory to support collision, picking, geometry editing or physics. If you don't need to use these functions, you can call this function to release the related memory:

scene.clearCachedVertexData();

5.3 Grid Import: Static Resources

If your mesh will not change position, rotation or size, it can be very effective to "freeze" the mesh by calling:

mesh.freezeWorldMatrix();

Even if they do change, but intermittently, then you can turn world matrix calculations back on by calling:

mesh.unfreezeWorldMatrix();

5.4 Mesh Import: No Interaction

If the user is not required to click or pick the mesh, the following line can be added when importing the mesh to further enhance performance:

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

5.5 VueJS and BabylonJS

After our project was finished, we actually discovered a pitfall of using BabylonJS and VueJS. While we're pretty happy with the FPS/performance here, it turns out we're 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's covered in great detail in this forum post.


Original text link: Babylon.js large-scale scene optimization practice - BimAnt

Guess you like

Origin blog.csdn.net/shebao3333/article/details/132202528