We’ve just released Babylon.js 2.3 with the biggest set of features we ever shipped
David (the other one) presented on his blog the demo we built with Michel Rousseau: The Sponza demo. This demo can really showcase what can be done on the web today regarding visual quality rendering (Even on low end devices).
Among all the new features we added, the one I’m the most proud about is dynamic point light shadows (DPLS).
To better understand what DPLS are, here is a small video of what they look like:
You can even see it live on babylon.js website: http://babylonjs.com/demos/SponzaDynamicShadows/
During this article I’m going to first explain what points lights are and then how we can achieve having realtime shadows in 3d.
Lights in 3d are used to produce the color received by each pixel. This color is then used by materials to determine the final color of every pixel.
They are various kind of lights and you can think about point light like a light defined by a single point. This point emits his energy in every direction (Like a really tiny sun).
Here is a super simple 3d scene with just a plane and a point light:
We can feel where the light is by looking at its impact on the ground.
Without a light the same scene would look like this:
Lights are a key point in 3D because it is really hard to feel any volume without a light.
Let’s take a simple red box for instance:
Now the same box with a simple light:
As you can see lights help your brain feel the 3D.
To get even more realism, you can add shadows to your scene. However shadows in 3D are hard to reproduce because they involve the concept of occlusion.
Indeed, a shadow is just a place where the light is occluded.
This implies the need to be able to check if the light can actually be reached or not for every pixel you want to render.
If you look at this new scene, you will see a sphere and a box with a point light but no shadow:
By default, shadows are off because they required a lot of GPU power.
Here is the same scene but now with shadows enabled:
Babylon.js makes this incredibly easy to do by allowing you to create a ShadowGenerator, define who casts shadows and who receives them, and rendering it.
Boom you’re done.
Points lights + shadows
Under the hood, things get a little bit more complicated.
To determine if a pixel is inside a shadow, we need a first pass where we will render the scene from the point of view of the light. This pass will generate the shadow map.
This map will contain the distance between every pixel visible from the light and the light itself.
During the main pass, we will use this shadow map to check if the distance of the current pixel and the light is greater than the one store inside the shadow map. This is done for every pixel. If this is the case, then the pixel is in the shadow:
The problem with point lights is that they emit light in all directions. You cannot simply generate a texture from the point of view of the light because there is no single point of view (i.e. a position and a direction).
This is where the point light shadows enter the game.
To fix this issue, we need to generate 6 textures: one for each direction (up, down, left, right, front and back). This way, the engine can always find the best texture to use depending on where the pixel to render is regarding the light.
For instance, here are the textures generated on every frame for the Sponza demo in my video (we call them cube textures, because, well, they form a cube):
Going back to the demo…
Now let’s get back to the demo to see how Michel built it.
First of all, you can go to the url and use the bottom control panel to enable the debug layer. it will allow you to play with engine’s parameters:
Let’s start from scratch by turning lights off:
Because the scene is based on purely dynamic lighting, everything turns black(like in the real world!).
Let’s then turn the lights on again and enable clickable labels to display all the entities living in the scene:
Lights are displayed in yellow and you can click on the labels to turn them on and off (Omin001 and Omni002).
So basically, the scene is built like this:
Final rendering with shadows
Based on what we saw before, there are 2 cubemaps generated for this scene.
But because only Omni001 moves, we can generate cubemap for Omni002 at startup and save a lot of bandwidth this way.
Looking at the future with WebGL 2.0
The current version of Babylon.js uses WebGL 1.0 so it requires 6 passes to generate a complete cubemap.
To increase rendering output, we plan to use an extension (which is part of WebGL 2.0) named “WEBGL_draw_buffers”. This extension will allow us to render all faces of a cubemap in one call improving a lot the overall performance.
In the meantime, we are really happy with the current technique which works on all modern browsers and devices which support WebGL 1.0.
During this article we saw why lights and shadows are important in realtime 3D. We also discovered how to add realtime shadows to point lights using cubemaps.
If you liked it you may find these links interesting: