Bringing balance in the force: A story about simplicity and control
One of the fundamental concepts I wanted for Babylon.js from the beginning was to allow users to do what they want in a simple way.
Our initial motto was ‘One feature, one line of code,’ and we have tried to stay true to that ideal ever since.
The idea was that the user should declare their intent, and then the engine would figure out how to render it in a performant way. You want shadows and post-processes? No problem — let me schedule everything for you while optimizing the process to remove unnecessary computations.
Layers of complexity
Of course, over the years, we added so many features that can be intertwined or interdependent. This led us to build a rather complex rendering loop within the scene.render()
function. The first layer for the user is to simply ignore this complexity and only declare what they want by creating objects and setting properties. The scene will then orchestrate everything to render it correctly. This is why the scene must be passed to almost all constructors, so it can keep track of the overall state.
However, as time passed, we began to have more advanced users who wanted greater control over the rendering process, which led us to introduce the second layer: booleans that can turn off certain aspects of the pipeline (such as scene.animationsEnabled
to entirely remove the animation step, or mesh.alwaysSelectAsActiveMesh
to skip culling for that mesh). Most parts of the rendering pipeline can be toggled on or off thanks to these switches.
To provide even more control, we then introduced the third layer: Observables. These tools enabled an incredible level of control because you can register to an event and change the state of the engine accordingly. For example, you can register for an event when a mesh is about to be rendered, disable certain features, and restore them after the mesh is rendered. This layer is really powerful, but I have to admit it isn’t the easiest to maintain since anything can happen in the observables. 😄
Here is a short example: Babylon.js Playground (babylonjs.com)
sphere.onBeforeRenderObservable.add(() => {
engine.setColorWrite(false);
});
sphere.onAfterRenderObservable.add(() => {
engine.setColorWrite(true);
});
The new stage we want to add will give you even more control. We want to let you create your own rendering sequence!
With great control comes more work!
With the latest version of Babylon.js, you can now override the scene.render
function and provide yours:
Babylon.js Playground (babylonjs.com)
scene.customRenderFunction = () => {
// Clear back buffer
engine.clear(new BABYLON.Color4(0, 0.3, 0.5, 1), true, true);
// Update view and projection matrices based on active camera
scene.updateTransformMatrix();
// Set viewport size
engine.setViewport(camera.viewport);
// Compute world matrix and render
for (let index = 0; index < scene.meshes.length; index++) {
const mesh = scene.meshes[index];
mesh.computeWorldMatrix();
mesh.directRender();
}
}
But as you can see you have to take care of everything (clearing, setting up the states, etc…)
For instance, if some materials are transparent, it is on you to enable the correct states:
Babylon.js Playground (babylonjs.com)
scene.customRenderFunction = () => {
// Clear back buffer
engine.clear(new BABYLON.Color4(0, 0.3, 0.5, 1), true, true);
// Update view and projection matrices based on active camera
scene.updateTransformMatrix();
// Set viewport size
engine.setViewport(camera.viewport);
// Compute world matrix and render
for (let index = 0; index < scene.meshes.length; index++) {
const mesh = scene.meshes[index];
if (mesh.material.needAlphaBlending()) {
engine.setAlphaMode(BABYLON.Constants.ALPHA_COMBINE);
} else {
engine.setAlphaMode(BABYLON.Constants.ALPHA_DISABLE);
}
mesh.computeWorldMatrix();
mesh.directRender();
}
}
And of course you will have to also render the transparent AFTER the opaque:
Babylon.js Playground (babylonjs.com)
scene.customRenderFunction = () => {
// Clear back buffer
engine.setAlphaMode(BABYLON.Constants.ALPHA_DISABLE);
engine.clear(new BABYLON.Color4(0, 0.3, 0.5, 1), true, true);
// Update view and projection matrices based on active camera
scene.updateTransformMatrix();
// Set viewport size
engine.setViewport(camera.viewport);
// Compute world matrix and render
const transparents = [];
for (let index = 0; index < scene.meshes.length; index++) {
const mesh = scene.meshes[index];
mesh.computeWorldMatrix();
if (mesh.material.needAlphaBlending()) {
transparents.push(mesh);
continue;
} else {
}
mesh.directRender();
}
engine.setAlphaMode(BABYLON.Constants.ALPHA_COMBINE);
for (const mesh of transparents) {
mesh.directRender();
}
}
And this goes with every feature you will want to add. We are giving you the choice to either go with a fully functioning car or a build your own car. It is up to you! Control vs Simplicity. You can choose where to set the cursor.
One more thing
But this is not all. In the upcoming weeks, we will release the very first (and still really experimental) version of our Render Graph: A new system that will let you design your rendering pipeline by connecting nodes!
Here is a very very early sneak peek to get you excited: