Creating thousands of animated entities in Babylon.js
RTS games were one of my favorite types of games growing up, I really enjoyed been able to control large amounts of units and thinking about strategies in real time with my friends. The idea of being able to control and entire fantasy army on an epic battle like the ones described in Lord of the Rings was always somethings that sounded very epic in my head.
Now that I’m part of the Babylon.js team, one of my first side projects was to try to use the engine to build those types of games. Of course, building an RTS is a lot of work, but by doing it in baby steps I was also able to learn some very useful things that I would like to share with you guys that might be interesting in doing something similar.
Optimizing the rendering
Rendering thousands of units can get pretty expensive, especially in the rendering loop side. If those units were completely different from each other, the engine would have to do a lot of setup for the rendering of each one (binding vertex data, setting up material parameters, etc…). Luckily for us, a lot of those steps can be skipped when rendering the same mesh. Using techniques like “instancing” allows game engines to minimize the number of draw calls to the GPU during the rendering loop.
There are multiple ways of doing instancing in Babylon.js. With each approach one might get more performance benefits in exchange for finite control over the rendering of the entity.
No Instance: Without using any type of instancing Babylon.js will need to setup rendering from scratch during the drawing of each mesh, this means that the binding of the index and vertex data, as well as material settings may need to happen for each mesh, resulting in much more calls to the rendering API. Things like WebGPU and internal optimizations of the Babylon.js engine will try to minimize this as much as possible, but the room for optimizations is much smaller when we are not able to make any assumption on how the mesh is setup.
Instances: Instances are the first level of optimization we can do when rendering multiple meshes. Instances can be created from any mesh using the createInstance method:
var newInstance = mesh.createInstance("instanceName");
Instances will have the same material as the root mesh (this is the tradeoff where the performance is coming from), but it will have their own transforms and collisions events. The following properties can be set individually for each mesh instance:
position
rotation
rotationQuaternion
setPivotMatrix
scaling
Due to the restriction on all meshes using the same material, instances allow us to skip many steps when rendering the meshes, it also gives the engine the clue that those meshes should be rendered together.
For more information about Instances please visit the official Babylon.js documentation: Instances | Babylon.js Documentation (babylonjs.com)
Thin Instances: Thin instances are the next level of performance improvements that can be used when rendering multiple meshes. It is way more restrictive than normal instances and we don’t even have access to a individual “ThinInstance object” to manipulate. Thin instances allow us to specify an array of properties to set for a mesh object, and an instance of that mesh will be rendered for each property on that array. This will allow the shaders to use each different value property when rendering each instance.
See more at : https://doc.babylonjs.com/features/featuresDeepDive/mesh/copies/thinInstances
The underling shader will then be able to use those property values when rendering each instance. The main buffer used when rendering thin instances is the “matrix” buffer. This buffer will define the world matrix for each instance. We also have buffers like “color” to set more properties on how those instances will be rendered.
Adding movement
To manage the movement of our units I will be using the RecastJSPlugin for Babylon.js, this will allow us to use an efficient navigation system to control multiple agents.
Since we are using thin instances for our rendered meshes, the navmesh agents will not be directly related to our mesh. We will be manually updating the world matrix of each thin instance with the values from the nav mesh agents (See the “UpdateThinInstanceBuffers” function on the playground sample).
For more information on Babylon Mesh Navigation system please see the link bellow:
Creating A Navigation Mesh | Babylon.js Documentation (babylonjs.com)
Animation
Rendering static multiple static meshes using instances is good and all, but in our RTS game we would like our units to be animated as well. Once again, since thin instances behave differently than normal mesh instances, we must handle animation a little bit differently.
We will Baked Texture animations to store our animation data into a texture and use the “bakedVertexAnimationSettingsInstanced” thin instance buffer to set which animation should each instance be playing.
For a more in depth explanation about Baked Texture Animation please see the following link:
Baked Texture Animations | Babylon.js Documentation (babylonjs.com)
Final Result
Finally, we can see the result of all those techniques used together in the following playground demo:
As can be seen in the demo, 1000+ animated entities can be rendered simultaneously. This shows how the proper usage of optimization techniques available on Babylon.js can really make a difference depending on your target.
I hope you all have enjoyed our little performance adventure! If you have any questions or comments, please reach out on the Babylon.JS forum!
Sergio Ricardo Zerbetto Masson — Babylon.js Team
(14) Sergio Ricardo Zerbetto Masson (@ZerbettoMasson) / Twitter