Babylon.js large-scale scene optimization practice

In this article, we'll focus on the optimization and architectural techniques used to optimize the Babylon.js harbor scene. In total our scene has over 600 meshes and 1,000,000 vertices. On our 2018 Macbook Pro it consistently runs at 45+ FPS in Google Chrome. We found Firefox to be around 40 FPS, while Safari was 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 that have 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 be used as well.

Note: There are many more optimization techniques available 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 official article on the subject .

1. Single and multiple model import

There are trade-offs when designing assets for a scene. Take our Harbor as an example.

insert image description here

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 the Babylon render loop has less to cycle through. 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 structure our individual assets (buildings, trees, etc.) in our own .gltf files, then import those files and use JavaScript to position them. This is not as practical as in your own 3d software in terms of designing layout and space, but gains in load time as models can be loaded in parallel. It can also easily take advantage of Babylon's own instancing capabilities, further optimizing tree placement.

In the MAS digital experience, we chose a hybrid approach. Most of the time we rely on assets built outside of Babylon.js, but within Blender. We found that giving non-developers the freedom to edit scenes was too useful to sacrifice. We're happy with the load times, as the experience always preloads with an intro animation, so the scene loading happening in the background is less noticeable. Also, we knew we wanted a high quality render of our scene in Blender. Building the full scene there means the static rendering perfectly mirrors the browser-based world.

2. Use Babylon.js to monitor performance

In order to optimize, we need to pay attention to 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, which is always in the corner. The Babylon engine can expose this value.
insert image description here

When we add new assets to the scene, it becomes very clear when we are doing something that is harmful to performance. Additionally, this provides a very easy-to-read indicator of improvement when consciously optimizing our scene.

Be sure to interact with your scene while doing this, though. Just because your 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 find where the performance impact is happening, we've found these very useful Babylon features in the Inspector that can help:

2.1 Wireframe and Point Rendering

In Babylon's Inspector we have the option to change the render mode. You can see the inspector by including the following line in your 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 provide very sharp polygon density images.
insert image description here

In this case, we can see that the building in the center of the image represents IBM's Operational Decision Manager, which is still poorly optimized given the number of sides and polygons it contains.

2.2 Show/Hide Grid and Enabled Toggles

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.
  • The "IsEnabled" switch 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 groups of objects to a single Empty creates a transform node. These are especially useful here, as when importing into Babylon, the "isEnabled" option can be toggled on/off for each node, so all child nodes in one click, making finding potential performance improvements much easier. Blender assemblies are lost when exporting to gltf.

With full scenes enabled, we hit 48 FPS.
insert image description here

When turning off the "isEnabled" option in the Inspector for our production-related buildings, there is very little impact on FPS - now at 49 FPS.

insert image description here

However, when turning off our "filler" buildings, we noticed a dramatic improvement, with the frame rate jumping to 59/60 FPS. Obviously, these meshes are where we need to focus our attention and optimize further.

3. Standard 3D optimization technology

We should follow standard good practices here that apply to any real-time rendering scenario, whether Babylon.js or browser-based.

3.1 Polygon count

The easiest metric to consider is polygon count. Rendering 4 vertices is much faster than rendering 4,000,000. When possible, be smart with your models and only add detail where it matters. There's no point in sculpting an HD face on a character model if your camera zooms out to 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 rendering and high-budget movies is great, but if you want to build browser-based experiences, be flexible about where and how your models provide detail.
insert image description here

"Before and After" view of rock model retopology

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 buildings have details like windows or doors, then you can also bake normal maps to give the look of the details without reducing the vertex count. Normal maps are especially effective at adding high detail without sacrificing polygon count.
insert image description here

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

It's worth noting that the .gltf mesh has triangular faces, so don't be surprised to see a large "face count" jump when you move the model to Babylon. Where possible, triangulate faces before importing to ensure you have control over polygons and face counts.

3.2 Instantiation

As mentioned above, it is faster to render 4 vertices than 4,000,000. One way we can "trick" our renderer into thinking we're using fewer vertices is through instancing. This reduces the total number of draw calls Babylon has to go through 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 case.
insert image description here

Instancing is a great way to draw lots of identical meshes (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, which can be achieved with a linked copy ), or directly in Babylon.js if your scene fits the use case of positioning objects from within javascript.

A negative consequence of instantiating and reusing the same model multiple times in a scene is that you're obviously flushing and reusing the same mesh over and over again.

Here we share some example renders from Blender showing how to take advantage of instancing while maintaining the sense of scale and variability of the model.

insert image description here

Left: 16 vertices (instantiated); 16k vertices (none) - middle: 48 vertices (yes); 48k vertices (none) - right: 228 vertices (yes); 228k (not included)

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 modified the building to make it asymmetrical and introduce a random rotation around the vertical axis. These small changes bring variability while maintaining an optimized geometry.

4. Additional considerations for WebGL - material merging

When using Babylon, files are usually imported using the .gltf format. You may notice though that when the model is loaded into Babylon it splits i's model into separate objects.

This happens on a material-by-material basis, which means 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 is to use as few materials as possible, and while you may want to have nice 4K textures and PBR materials for each object, you can use palettes if you can and fit the desired aesthetic.

insert image description here

Left: Various color palettes 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 faces of the model are aligned with the color you want.

To do this, in your 3d software, UV unwrap the object, shrink 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're using Blender, Imphenzia does this a lot in his 10-minute Blender challenges, and he details the technique here .

Each palette requires only 3x3 pixels (9 different colors) or 8x8 pixels (64 colors). You can also choose a larger palette, which gives you the option to add gradients to the palette. As shown above, we used this technique for the color palette of the port in the Mayflower experience.

5. Babylon.js tips and tricks

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

  • Optimization Scenario: link
  • Reduced memory footprint: link

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

5.1 Scenario: Disabling Object Interaction

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

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

We used this approach in our Harbor and Challenge 3 scenes. We can't use this in Challenge 1 because we need to detect user clicks on environment objects (rocks, buoys, and boats).

5.2 Scenario: Remove cached vertex data

All vertex buffers keep a copy of their data in 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 Mesh Import: Static Assets

If you have a grid that doesn't change position, rotation or size, it can be very efficient to "freeze" the grid by calling:

mesh.freezeWorldMatrix();

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

mesh.unfreezeWorldMatrix();

5.4 Mesh Import: No Interaction

If you don't need users to click or pick your mesh, you can further improve performance by adding the following line when importing the mesh:

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

5.5 Combination of VueJS and Babylon.js

After our project was finished, we actually discovered the pitfalls 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 the details here, but it's covered in great detail in this forum post .


Original Link: BabylonJS Large-Scale Scene Optimization—BimAnt

Guess you like

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