As many of you knows, JME 3.0 only has a Forward rendering pipeline. One of the major drawbacks of such a pipeline is that it basically does one geometry rendering pass for each light in the scene. Typical complexity of a scene in JME is (number of objects x number of lights).
This leads to big performance issues when you have several lights in a scene, that many of you experienced. Even with a fairly low amount of objects, many lights can quickly be a killer.
Until now the ways to alleviate the issue were to bake lights in textures, partition the scene graph and attach lights to sub nodes… or implement your own deferred rendering pipeline… (more on this later).
We wanted to offer better and easier solutions for users.
The first new feature is light culling. It’s based on the observation that rendering a light that is not in the camera frustum or rendering an object for a light that doesn’t affect it is a waste.
jME now has a way to sort that out. On each frame, each light is checked once against the camera frustum and not rendered when it’s outside. Also each geometry is checked against each light in the frustum and only renderer if the geometry is in the area of effect of the light.
For example, in today’s jME, figure 1’s typical scene would yield 6 draw calls : 3 for the red sphere, and 3 for the blue (1 for each light in the scene L1,L2 and L3).
With light culling this results in 3 draw calls : 1 for the red sphere ( only affected by L3) and 2 for the blue sphere (once for L3 and once for L2).
So how do we do it? The RenderManager now has a LightFilter, that is called every time we want to render a geometry. There is a default implementation set (DefaultLightFilter).
This Filter extract the lights list to render for this frame for a given geometry. Of course each type of light have different way of checking their intersection with the frustum or with a geometry.
If you want to disable light culling it’s safe to set this filter to null.
Here is a scene featuring 18 geometries and 18 lights. The grey ground is one big geometry, each blue cube is a geometry and the strange guy in the middle is one geometry. There is a directional light, a blue spot light in the center, and a point light above each cube.
In the first view you can see the whole scene rendered without light culling, the second is a subpart of the scene without light culling.
As you can see the performance gain is not huge when the whole scene is in the frustum, even if the number of geometries drawn is cut in half. The reason is that the culling itself has a cost, because it iterates over every lights for each geometry, and perform a bound check.
The gain is more substantial when looking at a sub part of a scene, Culling out lights from the frustum definitely yield a performance boost.
The geometries in the scene are very simple here, and the speed boost might be higher with more complex geometries. Also with more spread out lights in a bigger scene the gain is definitely higher when looking at a sub part of the scene.
But… This doesn’t dispend you to partition your scene and only light sub part of the graph with each lights. The culling will be a lot faster.
Shadows now also use light culling. This means that shadow maps won’t be calculated for lights that are outside of the frustum. This can give a huge performance boost.
Light culling is nice, but it’s not an absolute revolution in our rendering pipeline. We still have to render an object once per lights. Let’s get rid of that.
The idea behind single pass is to send a list of light information to the shader and to compute all the lights contribution while rendering the geometry once.
It’s been a long time that our material system had a way to select multipass or singlepass lighting. But the lighting shader was never implemented to support it. Now it’s done for Lighting.j3md and for TerrainLighting.j3md
You can set the preferred light mode on the renderManager. If you set it to SinglePass, the material will preferably select a technique that supports it.
In practice, you cannot just send ALL lights of the scene to the shader. Light data is sent as a uniform array and you have a limit in size that depends on the hardware. To alleviate this issue we decided to do one pass for a batch of lights. The size of the batch is configurable, and has a big impact on performances. I’ll explain how to find the right value in an more detailed documentation on the wiki.
It the same scene as above, I’ve used single pass with a batch size of 6.
Compared to what you have in v3.0 (MutiPass lighting, with no light culling) rendering this scene in SinglePass with light culling is overall 70% faster.
Pro and cons
There are not a lot of cons to be honest, but single pass require that the lighting calculation occurs in the fragment shader. This means that if you have only one light in your scene, Multipass lighting will be faster. But, computing lighting in the frag shader yields a more precise lighting, so even with a single light, you may have a render quality increased… you decide ;)
How does it perform compared to Deferred lighting?
I honestly don’t have a clue. But that can be a nice alternative if you have a moderate amount of lights, and avoiding all the querks you’ll hit with deferred rendering (difficulties for AA, transparency, material versatility, big memory bandwidth, etc…)