Optimizing scenes for better WebGL performance
Here we recommend some optimization techniques that proved to work well for creating web-based interactive experiences. This chapter is mostly based on Soft8Soft's presentation at the conference Verge3Day Europe 2019.
- Geometry / Meshes
- Normal Maps
- Vertex Colors
- Number of Shaders
- Draw Calls
- HDR Lighting
Geometry / Meshes
Geometry lies at the root of a 3D application as it forms the main shape of a model. To get smoother reflections and faster rendering you should keep the mesh as regular as possible. In the beginning, you should decide on the level of details you want to have in your scene, and stick to that when modeling.
When modeling creases, better use smooth groups instead of adding more polygons.
When working on a cylindrical model, make effort to reduce the number of polygons by its center.
Do not overload a model with extra details that the user won't see anyway. As shown on the picture below, the edge highlighted with orange defines the level of details for the whole model, so you can use at as reference.
A common way to optimize WebGL performance is to reduce the amount of polygons by baking a normal map from a high-poly model to a low-poly model.
However, normal maps may produce visible artifacts due to the limited precision of a 8 bit image. Some possible solutions are pictured below, but they are rather impracticable: using a higher precision image would produce a heavier file, while the second approach is rather time-consuming and does not guarantee a clean result. The third approach however may work in some cases: if you have rather rough surfaces we recommend to add noise to your materials to reduce those artifacts.
From our experience, we found that the best solution for glossy objects would be using middle-poly geometry with smooth groups, and without any normal map.
Finally, here are some cases when you might want to use a normal map rather than a highly detailed mesh:
- Your object consists of many varying surfaces.
- You have a rough surface that does not produce precision artifacts.
- Your objects are distant or small so the user won't notice any artifacts.
Here is a typical set of textures used in the PBR pipeline (and in general).
As you can see, most of them are black and white. Therefore you may combine b/w textures into the RGBA channels of a single image, up to 4 maps per image.
If you only have one b/w texture you may combine it with any existing RGB texture by packing it into the alpha channel. Finally, if you have no image to combine with, you can convert your b/w image into jpeg format with 95% compressing and the grayscale mode enabled.
Another way to reduce the size of texture is to optimize the UV space. The more compact is your UV unwrapping, the more effectively your image will use the texture space. Therefore you can have smaller images without losing any quality.
Using vertex colors instead of images is an efficient way to speed up the loading and improve the overall performance of your WebGL applications. Although it comes at the expense of additional edges which you have to add to your model in order to separate different vertex colors.
You can also use vertex colors to define roughness, metalness or specular surfaces, or any other parameters. Below you can see an example of such a material where only vertex colors are used.
Number of Shaders
This is very beneficial to have less different materials/shaders in your scene. Shader processing in WebGL leads to prolonged loading, which is especially noticeable on Windows. Also if you have less shaders, the engine will spend less time on switching between them while rendering, thus improving the performance.
Here's an example of such the optimization. All these tires are represented by only one material and configured by swapping its textures.
In order to reduce the number of shaders, you can combine 2 or more simple materials into one bigger material. This technique is especially effective if you plan to switch between these materials (e.g. you are making a configurator app), because it works faster this way and also allows for animated transitions.
In addition, there is another important aspect - the number of draw calls. This number can be obtained from the Geometry Buffers section of the print performance info puzzle's output. This roughly corresponds to the number of separate objects if only one material is assigned per object, while multi-material objects require even more draw calls to render them.
Therefore, you should strive to join meshes when possible, and use less unique materials, in order to reduce the number of draw calls and improve the performance.
If you have an animated object, you can still join its parts together and use bones for animation, which is sometimes even more convenient when animating separate objects.
It helps a lot improve the performance if you lighten your scene by an HDR image only, without using any light sources. An HDR file may weight less than 1 Mb.
Use dynamic shadows only when they help present your object nicely. On the picture below, dynamic shadows used in the Industrial Robot demo emphasize the shape of the robot model. The model itself is allowed to be rotated so the shadows cannot hide any part of the object from the user. On the other hand, shadows in the Scooter demo would cloud many details.
If your object does not move in your app, you may bake shadow and ambient occlusion maps and apply them to the plane under the object.
Check out the Asset Compression chapter to learn how you can make your apps even more compact.
Feel free to ask on the forums!