Hi everybody, this is going to be a short post with some tips on how to drastically (in some cases) reduce the usage of the GPU when using 2D lights and shadow casters in your scenes. I have read many people who complain about the bad performance of the 2D lighting system, without knowing why or what are they doing wrong. I have also read other well-meaning devs that spread some tips that are not true at all.
After a lot of time digging and analyzing the code of the 2D Renderer, I came to some conclusions that I will summarize in the following paragraph. In the near future, if I have enough time, I will explain how the 2D Renderer works internally in detail (until the version 2020.3.4).
Define sorting layers in a contiguous way
Contrary to what some people say, you can have as many sorting layers as you want, and that will not affect (in a noticeable way) the performance of your game. HOWEVER, those sorting layers that are affected by the same lights must be consecutive in the list where you define them, in the editor.
Let’s suppose you have 5 sorting layers:
-
FarBackground
-
NearBackground
-
Props
-
Characters
-
Overlay
Now let’s suppose all your “normal” lights affect in the same way the sprites in the sorting layers NearBackground, Props and Characters. The lights texture will be generated only once, despite they are 3 layers. You could have 500 layers instead of 3 and there would not be any significant difference. The system identifies blocks of lights that affect consecutive layers, so there would be only 1 block in the example.
Now imagine your list looked like this (new layers would not be affected by lights):
-
FarBackgorund
-
NearBackground
-
UnlitNearBackgorund
-
Props
-
UnlitProps
-
Characters
-
UnlitCharacters
-
Overlay
With this setup, the system would check the target sorting layers of the lights, one by one, from the first (furthest/deepest) to the last. It would find that NearBackground is affected by all the lights, but then it would see that UnlitNearBackground is not. All the lights that affect NearBackground would be rendered to the light texture (including the calculation of the shadow casters), the sprites in that sorting layer would be rendered using that texture, and the system would continue checking next layers. The same would happen to the Props layer, the same lights would be rendered (and their shadows), and the same would happen again to the Characters layer. All lights and shadow casters are calculated 3 times.
Combine shadow casters
For each light that is to be rendered (and whose Shadow Intensity > 0) all the shadow casters that match the same target sorting layers will be calculated. So it is better for you to place as less ShadowCaster2Ds in your scene as possible. This does not mean that you must resign yourself to not using them, instead this means that you should find a way to collect all the meshes generated by most of the shadow casters and combine them into one, so you are only calculating one shadow per light in the best case, no matter how many polygons it consists of. This is a big performance booster.
Set Shadow Intensity to zero, when possible
If a light is not supposed to cast shadows because it is far from any shadow caster or the shadow it casts is barely visible, you should set its inspector property “Shadow Intensity” to zero. This way, when rendering the light, no shadow caster will be calculated.
Some last comments, based on my experience
- It does not matter how many sprites are affected by lights, they will be rendered in the same way, being lit or not.
- I recommend that you define a small set of “light interactions”, I mean, which contiguous-sorting-layer-blocks are all your lights going to use, and never change it. You could define one for characters and props (with N contiguous layers), and other for the background lights (with another N contiguous layers), for example.
- I recommend that ShadowCaster2Ds use exactly the same contiguous-sorting-layer-blocks as lights, do not make exceptions or different combinations. What happens if the target sorting layers of the shadow casters do not match exactly the lights’? Well… the lighting system have some bugs, and it is very probable the shadow caster will not be used in the process.
- I recommend using a kind of " Target sorting layers " asset that stores a contiguous-sorting-layer-block, so every light and every shadow caster uses that value when the scene is loaded. This way this standard value will be centralized and you can change it later in the entire project without having to change every object manually or running any automated tool.
Jailbroken uses 2D lights and shadows intensively. Following these rules I have achieved HUGE performance improvements which allowed me not to cancel the project or not to change its aesthetics drastically.
I hope this information it is useful for you:), I would have loved to know this before I started (Unity documentation, I’m looking at you).
You can find other of my posts here:
- LineRenderer2D: GPU pixel-perfect 2D line renderer for Unity URP (2D Renderer)
- Script for generating ShadowCaster2Ds for Tilemaps
- Target sorting layers as assets
- [Programming tools]: Constrained Delaunay Triangulation
You can follow me (and my game) on Twitter: @JailbrokenGame