We’ve just released Babylon.js 2.3 with the biggest set of features we've ever shipped.
On his blog, David (the other David!) presented the demo we built with Michel Rousseau: the Sponza demo. This demo really showcases what can be done on the web today regarding visual quality rendering (even on low-end devices.)
Amongst all of the new features we've added, the one I’m 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 the babylon.js website: http://babylonjs.com/demos/SponzaDynamicShadows/
During this article, I’m going to first explain what point lights are and then how we can achieve having real-time 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 as a light defined by a single point. This point emits its 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 tell 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 see 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 understand 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 require 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.
Point Lights and 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 stored inside of 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 in regards to 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 the engine’s parameters:
Let’s start from scratch by turning the 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 of 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:
Based on what we saw before, there are 2 cubemaps generated for this scene.
But, because only Omni001 moves, we can generate the cubemap for Omni002 at startup and save a lot of bandwidth this way.
Looking to 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 the overall performance a lot.
In the meantime, we are really happy with the current technique which works on all modern browsers and devices that support WebGL 1.0.
During this article, we saw why lights and shadows are important in real-time 3D. We also discovered how to add real-time shadows to point lights using cubemaps.
If you liked this article, you may find these links interesting: