UPDATE - Part 2 is online

So, what are we talking about?

The goal of each canvas-based framework on the web is to reach the holy 60 FPS rendering. The more features are being added to the party, the slower the rendering will be. One of the more CPU-consuming tasks in BabylonJS is the native collision system, that prevents a camera or a mesh from going "through walls". A much-needed feature in a game engine.

A simple way of seeing the integration of the collision process (of a camera) would be:
Rendering loop with collisions

The scene won't finish rendering until the collision sends a new camera position. The longer it takes, the less FPS we achieve. With simple meshes this shouldn't take long. But since Babylon is checking collision against the entire mesh structure (and not only against its bounding box or sphere) this can take a while with complex meshes in the proximity of the camera.

Integrating workers

Without explaining what workers are (maybe in a future post!), a worker is the JavaScript way of parallel processing. It allows a computation to run in the background and keep the view (or main) thread "free" to render the actual view. Sounds like just the thing we need!

But! Workers have their limitations.

  1. Workers have a very limited scope of functions and classes they can naively use. Forget about the DOM, forget about your window object or the scene's mesh data. Mozilla has this very helpful functions/classes list that lists what's available to workers in each browser.
  2. Messages to and from workers are sent asynchronously. The JavaScript way! I actually see that as an advantage in many cases, but coming from multi-threaded languages, it is sometimes hard to understand.
  3. Worker has to be loaded from an external file. You cannot deliver a function to be executed. This is per design and is due to the 1st limitation. Let's say this was possible:
var someAwesomeVariable = 42;  
function myWorkerFunction() {  
    return someAwesomeVariable;
}

var worker = new Worker(myWorkerFunction);  

The variable outside of the function's scope is available inside the function. This is pure JavaScript. But sending this function to be executed in a worker means (according to the 1st rule of workers - nothing from the main thread) that the variable that was defined outside the function's scope will not be available there. For the browsers' developers this is a rather easy task - clear the scope, run the function, complain that the variable is not there. It is, however, very confusing for developers! Variables outside of the function's scope can be used even with promises. Why is it not the case with workers?

My assumption is that to avoid this confusion, the worker's specification states that a worker can only be started using a new file, not allowing an implementation like I've shown before. Which is totally wrong! please don't use it...

There is a way of loading a worker from the main thread using "stringed" functions and blobs, I will discuss this is a future post.

The limited scope problem

To calculate collisions properly, the collision systems needs to have the entire information about each mesh - it's global transformation, visibility, sub-meshes and of course its vertex data - especially positions and normals. And it should, of course, always have the updated information, since meshes' transformation is constantly changing as well as the camera's.

As I wrote before, the worker doesn't have this information, and it has to be sent to it. There are a few ways of solving this:

  1. Sending the entire information in each message sent to the worker. This way the worker is stateless - it calculates the collision according to the data collected and sends an answer.

    Pros:

    • Saves RAM - data is stored only once (in the scene itself)
    • easy to implement

    Cons:

  2. Caching the meshes in the worker itself, getting updates when a mesh or its geometry is updated.

    Pros:

    • Vertex data is only sent once (unless it is updated, which is usually not the case).
    • Eventually faster than the first option, because the worker keeps the cache and only gets updates.

    Cons:

    • RAM consumption (almost) doubles. The meshes' vertex information is stored in both the scene and the worker.
    • Require(d) changes to the framework - callbacks to when a mesh or geometry is added or updated.
  3. Storing the scene's data in a database available to both the worker and the main thread. The only database natively available to both is IndexedDB. I am a great believer that IndexedDB with Workers make a great couple. I have even created a BabylonJS plugin to achieve exactly that - storing Babylon's scene data in IndexedDB

    Pros:

    • Unified database available to running and future workers. scales wonderfully.
    • Data structure is defined and stored in a database, indexed according to the needed keys

    Cons:

    • IndexedDB's speed is very influenced by the client's state - it might be too slow for updating a heavy scene 60 times per second.
    • Each browser has its own IndexedDB implementation (as it always is). Some are faster than the other. I highly recommend these IndexedDB performance tests for further reading.

In part 2 I write about the development process that eventually led me to use the 2nd variant.

Asynchronous messages

I showed earlier a (highly professional!) activity diagram showing the process of collision detection. The collision detection system cannot be entirely asynchronous - it would make no sense to move the camera forward and only afterwards get the position correction. The camera will jump constantly back and forth.
Another async-mess would be due to the fact that the

worker.postMessage(...);  

command is storing the messages sent in a kind of a message queue and the worker processes them one after the other. In theory it is wonderful. In action it would mean that if the worker takes longer than a frame to process a message, the next message, that belongs to a frame before will be processed even thou this frame is already gone. It would look like this:
async between main thread and worker

Which would lead to some major problems in calculating the collisions.

A certain synchronization had to be achieved.
In part 2 of this article I explain how I solved this and more.

Connect with me on Twitter or LinkedIn to continue the discussion.

I'm an IT consultant, full stack developer, husband, and father. On my spare time I am contributing to Babylon.js WebGL game engine and other open source projects.

Berlin, Germany