In part 1 we have discussed about the way the current collision detection works in BabylonJS and about workers in general. The main issues were:

  1. Getting the updated mesh state to the worker, as it can't access the window scope.
  2. Letting the render process continue while the worker calculates the collisions.

Before I explain about those issues, a small note - the worker collision is no magic that will solve all of your low FPS problems. It will also not make the camera magically move fluently in a fully loaded scene. The actual collision calculation is using the same code that is being used in the regular collision. Nothing new was implemented there.

Now let's see now how those issues were addressed and solved. we'll start with:

Mesh serialization and worker updates

Tl;dr - Updates in the meshes' and geometries' state are collected and sent to the worker, which caches the metadata sent to be able to correctly calculate collisions.

What data is needed?

To be able to calculate collisions efficiently, Babylon's collision system is calculating collisions of the ellipsoid of the colliding object against all of the complex meshes in its area. To simplify this - think about your camera as a box in space which is trying to go through the scene's meshes.

In order to compute collisions, the meshes' metadata and geometry must be sent to the worker. The needed information for each mesh is:

  • World matrix (position and rotation)
  • collision enabled flag (should collisions be calculated for this mesh)
  • Bounding box and sphere (for coarse collision detection)
  • ID/Name/UniqueID of the mesh (to identify the mesh and return a correct result)
  • Submeshes (including all of their meta data)
  • Vertex Data:
    • positions
    • normals
    • indices

All of this data must be present for each mesh for each collision calculation.
Further important information involves the colliding object's position and ellipsoid data (can be a camera or a mesh moving with collision). No vertex data is required, as only the "box" around the colliding object is needed.

Serializing and updating the worker

I have decided to send two types of "commands" or messages to the worker. An update message and collide message. I also separated the sent data to two sub categories:

  • metadata
  • geometry

I did that for two reasons:

  1. The mesh's metadata (including its world matrix) updates much frequently than the geometry.
  2. The geometry can be relatively large - sometimes a few MB of data. Depends on the mesh's complexity.

For that I have created a few typescript interfaces (to ease the coding. I have to write one day about how much I love TypeScript :-) ):

    export interface SerializedMesh {
        id: string;
        name: string;
        uniqueId: number;
        geometryId: string;
        sphereCenter: Array<number>;
        sphereRadius: number;
        boxMinimum: Array<number>;
        boxMaximum: Array<number>;
        worldMatrixFromCache: any;
        subMeshes: Array<SerializedSubMesh>;
        checkCollisions: boolean;
    }

    export interface SerializedSubMesh {
        position: number;
        verticesStart: number;
        verticesCount: number;
        indexStart: number;
        indexCount: number;
        hasMaterial: boolean;
        sphereCenter: Array<number>;
        sphereRadius: number;
        boxMinimum: Array<number>;
        boxMaximum: Array<number>;
    }

    export interface SerializedGeometry {
        id: string;
        positions: Float32Array;
        indices: Int32Array;
        normals: Float32Array;
    }

Notice that the geometry information is using a Buffered Array objects (Float32Array). They will later be used as transferable objects. The rest are core JavaScript objects.

Each frame, the collision coordinator receives meshes' updates, collect them, and sends them to the worked using the "update" command. Doing that required a callback from both the mesh and the geometry objects.
The mesh had already an after-update callback mechanism. The geometry didn't and it was therefore added for version 2.1.

This collection of updates (which is sometimes empty, if nothing changed) is then sent to the worker.
Forst the data is being collected to a message object:

var payload: UpdatePayload = {  
    updatedMeshes: this._addUpdateMeshesList,
    updatedGeometries: this._addUpdateGeometriesList,
    removedGeometries: this._toRemoveGeometryArray,
    removedMeshes: this._toRemoveMeshesArray
};
var message: BabylonMessage = {  
    payload: payload,
    taskType: WorkerTaskType.UPDATE
}

The transferable objects are set using the following (TypeScript) code:

for (var id in payload.updatedGeometries) {  
   if (payload.updatedGeometries.hasOwnProperty(id)) {
       //prepare transferables
       serializable.push((<UpdatePayload> message.payload).updatedGeometries[id].indices.buffer);
       serializable.push((<UpdatePayload> message.payload).updatedGeometries[id].normals.buffer);
       serializable.push((<UpdatePayload> message.payload).updatedGeometries[id].positions.buffer);
     }
}

The serializable objects is then given as the second variable in the worker.postMessage(...) command to set the transferable objects and not serialize the large arrays.

this._worker.postMessage(message, serializable);  

The worker then takes this information, caches it in the Worker Cache object located at the worker's scope and returns a "success" answer for the main thread to know everything went ok:

for (var id in payload.updatedGeometries) {  
    if (payload.updatedGeometries.hasOwnProperty(id)) {
        this._collisionCache.addGeometry(payload.updatedGeometries[id]);
    }
}
for (var uniqueId in payload.updatedMeshes) {  
    if (payload.updatedMeshes.hasOwnProperty(uniqueId)) {
         this._collisionCache.addMesh(payload.updatedMeshes[uniqueId]);
    }
}

var replay: WorkerReply = {  
    error: WorkerReplyType.SUCCESS,
    taskType: WorkerTaskType.UPDATE
}
postMessage(replay, undefined);  

This way the worker is constantly updated and can calculate the collisions correctly. The entire process is registered as an after-render function in the scene and is executed every frame.

Since I am using transferable objects for the large (float-based) data, an update happens almost immediately. It is actually less interesting how long it takes for the data to get to the worker. The main interest is to keep the rendering process running while those tasks are executed. The worker.postMessage command returns less than one millisecond after it is called (on my computer, results may vary :-) ). Perfect for our usecase!

About my original idea - using IndexedDB

The first implementation I have created as part of BabylonJSX was using the IndexedDB backend for BabylonJS, a plugin I wrote that keeps all of the serialized mesh and geometry metadata in a database backend based on IndexedDB. I am very happy about the first version I have created, which worked wonderfully. The idea was to keep everything in a database that can be accessed using workers or even other windows. This way, the workers can scale easily - each worker has access to the same database. Without constantly updating the data in each worker, a worker pool of collision-detector-workers will be easy to implement. I am a great supporter of this indexeddb-worker combination. Actually, I think I am almost the only one...

I can, however, understand why David asked me to port the final implementation to transferable objects. The main reasons are:

  1. One less dependency for the framework.
  2. Each browser implements IndexedDB a bit differently. The API is the same, but speed varies. This is, of course, the same with transferable objects, but most browsers understood already the need for them and therefore implemented them well. IndexedDB, however, is rather new and unused. The browser devs probably don't see a reason to improve its performance.
  3. For large portions of data, transferable objects were quicker. We are talking about a few MB of data 60 times a second, but still, this is what needed.

I will continue testing IndexedDB. When I see that the DB is as quick as transferable objects (or at least quick enough for our needs) I will try convincing David to start using it.

Checking collisions asynchronous

Tl;dr - The collisions requests are being sent as command to the worker. The collided object (usually the camera) doesn't move until the worker finished its calculation and sent a new position, but eh rendering process is running without any interference.

Turning sync to async

In part 1 I showed this simple activity diagram:
render loop with collisions.

In order to let the engine keep on rendering while checking collisions, the 2nd and 3rd boxes should be extracted from this loop and move to the worker.
The new diagram will then look like this:

worker collision render loop

Small note - in the "Should camera move" part the next step will be triggered instantly even if the camera should move, as the worker.postMessage returns immediately.

You can see that the render loop will go on, even while the worker calculates the collision. The camera will only move after a new calculation is returned from the worker.

This is the explanation to the reason your camera still doesn't run smoothly in a complicated scene. The camera won't move until an answer was returned from the worker. It will stay in place and wait (patiently, I hope!) until the worker finished its calculation and returned a new position. The scene will keep on rendering even if the worker didn't finish yet.

How was it implemented

I have wrote about the way I have implemented the worker in a past article titled Automated build of web workers without a separate js file. This is a wonderful start. You can check the code at the babylon github repo. I would recommend looking at the collision coordinator which includes the object serialization functions and the many interfaces and enums used during the implementation, and the worker collider which includes the cache implementation, the message processing and the actual collision calculation. I will be more than happy to answer any questions in the comments.

Allowing legacy support

Even after adding this feature, the old (what I call "legacy" collision system still needs to function correctly. Some won't find the worker fast enough, some will understand that the regular collision is enough for their scene.

For that reason I have created the collision coordinator interface with the help of TypeScript:

export interface ICollisionCoordinator {  
    getNewPosition(position: Vector3, velocity: Vector3, collider: Collider, maximumRetry: number, excludedMesh: AbstractMesh, onNewPosition: (collisionIndex: number, newPosition: Vector3, collidedMesh?: AbstractMesh) => void, collisionIndex: number): void;
    init(scene: Scene): void;
    destroy(): void;

    //Update meshes and geometries
    onMeshAdded(mesh: AbstractMesh);
    onMeshUpdated(mesh: AbstractMesh);
    onMeshRemoved(mesh: AbstractMesh);
    onGeometryAdded(geometry: Geometry);
    onGeometryUpdated(geometry: Geometry);
    onGeometryDeleted(geometry: Geometry);
}

Two classes are implementing this interface, the worker version - CollisionCoordinatorWorker and the legacy version - CollisionCoordinatorLegacy. Switching between both of them in the scene will simply initialize the correct class.

But...

Wait, how do I use it?

The worker collisions is turned off per default. To turn it on you need to tell the scene to move to worker collisions:

scene.workerCollisions = true;  

And that's it. The setter will take care of the rest.

A bit more about the workerCollisions variable.

The workerCollisions boolean is actually a setter function, implemented (in TypeScript) this way (commenting in the code it self) :

public set workerCollisions(enabled: boolean) {  
   // If worker is not supported by this browser, enabled will always be false.
   enabled = (enabled && !!Worker)

   // Set the internal variable to true.
   this._workerCollisions = enabled;

   // Destroy the old collision coordinator. This is used to stop a worker, if it is working.
   if (this.collisionCoordinator) {
      this.collisionCoordinator.destroy();
   }

   // Create a new collision coordinator object, according to the "enabled" flag.
   this.collisionCoordinator = enabled ? new CollisionCoordinatorWorker() : new CollisionCoordinatorLegacy();

   // Init the coordinator, start the party!
   this.collisionCoordinator.init(this);
}

One again, if you have any questions about any of this, please ask in the comments, I will happily take the time to answer.

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