Welcome to VR Me Up developer log number 10. It’s been a little while since my last developer log, and that’s because I have been working hard on improving the performance of the WebXR Three.js client application and fixing some workarounds that I put in to try and improve the performance earlier. I can now render thousands more objects at almost twice the frame rate, allowing me to increase the complexity and viewing distance of the world. This developer log is about those performance improvements and some of the other things I discovered along the way.
The big change was moving from using individual Three.js objects to create the scene to using InstancedMesh objects instead. Before you think to yourself … “I know everything about instanced meshes”, I have some things I want you to consider.
- Object Field Of View Culling
- Level Of Detail.
On high end desk top machines, with dedicated GPU’s, these things may not have a large impact on performance, but on low end devices like mobile phones and embedded VR headsets, they can have an noticeable impact.
View Frustum Culling
Running the Three.js instanced mesh example on a Quest 2, and positioning the camera in about the centre of the instances, in naive mode we are getting around 85 frames per second for about 2600 instances. Each of Suzanne the monkeys heads is a separate draw call. Now, switching to instanced mode, the frame rate surprisingly drops to about 55 FPS, even though all the heads are now being drawn in a single draw call. Switching to merged mode, still does not improve the frames per second.
In native object mode, meshes are removed by the view frustum culling step in the Three.js render pipeline and never sent to the GPU. In instanced mode, however, all the meshes are sent to the GPU. Even though the GPU may not render the entire mesh, as it may be outside it’s triangle view frustum culling step, the mesh still has to go through the vertex shader. On devices with a low end GPU, this can actually decrease performance.
To solve this I allocate space for a maximum number of instances when creating the Instanced Mesh but I set the count to only render the number of instances that are visible.
var im = new THREE.InstancedMesh(geom, mat, 1000) ;
...
im.count = 700
On each frame I iterate over the instances and move the visible ones to the front of the instance array and the ones outside the view frustum to the hidden section and reset the count. I do this very efficiently using some array insert delete optimizations to reduce the number of reads and writes to the instance array.
// Setup the clipping frustum
var camera = this._camera = context.camera;
camera.updateWorldMatrix(true, true) ;
var mat = new THREE.Matrix4() ;
mat.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse )
frustum = new THREE.Frustum() ;
frustum.setFromProjectionMatrix(this.__mat4);
var bs = geom.boundingSphere
var testSphere = new THREE.Sphere()
...
// For reach instance position
sphere.set(pos, testRadius) ;
var isVisible = frustum.intersectsSphere(this._testSphere) ;
As a result I get the best of both techniques, I reduced the number of draw calls to draw multiple items at once and I only render the visible mesh instances.
Level Of Detail
The next issue I had with Instanced Meshes was that they do not support Level of Detail. This is a common scene optimization technique where distant objects are drawn using a less complex mesh in order to reduce the polygon and vertex numbers. All the Meshes in an Instanced Mesh are drawn at the same complexity, which can have an impact on performance.
Instead of using a single Instanced mesh, I create one for each mesh level of detail. Using a technique similar to the method I used for the “View Frustum Culling” I move the instances between the separate instanced meshes depending on the instances distance from the camera.
Using multiple InstancedMesh objects for each level of detail, combined with the View Frustum culling almost doubled my frame rate.
threejs-polygun NPM package
I decided to package all the code I wrote to do all this into an NPM package called threejs-polygun so that other people would be able to use it in their projects. I still have a few bugs to fix, so I have not released it yet. If your interested please follow me on YouTube or Twitter/X and I’ll let you know when it’s available. In the mean time, you can have a look at an example mega city project I created to get a feel of how it looks.
See you in the metaverse… bye for now!
VR Me Up.