top of page

Volume, Mesh and Mask Baked Shadows (Mobile - friendly)



I like cool glossy lighting. It makes virtual environment look real. But what really breaks immersion is when light illuminates object that sits on the other side of the wall from given light. That is why you'll see "Light Range" property for light sources in Unity and Unreal. Property which doesn't make too much sense physically since photons from even the tiniest candle may still affect surface far away. You would tune those to be constrained within the single room and never cast light onto objects outside of it. Range property is also there for performance reasons - not to waste performance calculating light from source for object that is hardly affected by it.


Light from behind the wall wouldn't be a problem if shadows are calculated - the wall will block the light. Real-time shadows are extremely demanding: For each source of light a big chunk of scene needs to be redrawn every frame to generate those real-time shadow maps. So you will not see more then 1 or 2 shadow-casting light sources at any single time even in modern games, since those games also tend to have more complex geometry, which will mean even more things to render for each of those lights.


There is already an elegant solution for things in your scene which do not move, and the solution is the Light Baking. Its an optimal solution, and perfect for many cases.

Baked Light

Place as many static light sources as you want, and the Engine will Bake them to one or many light textures stretched across all static geometry in your scene. The upside is performance: no way your phone could have calculated all that in real time. Downside is that baked light makes everything look flat, it is not affected by normal map, and you will not get those glorious glossy reflections, as with real-time lights.


Glossy reflections make scenes feel more alive. While the light itself naturally would illuminate just a small area around itself, you will clearly see it's reflection on more distant glossy surfaces. Glossy reflection is like that white dot in an eye of every cartoon character, telling you that he is alive - because when we die, our eyes dry out fast and are no longer glossy. That is where the term "The light left his eyes" comes from. So the method I'm developing is literally a matter of life and death ;)


Baked Darkness

So instead of baking the light I decided to try and bake the shadows: RGBA channels of the texture will contain shadow data for 4 different sources of light. Actually I decided to use A channel to paint ambient occlusion, which will reduce the ambient light, but that is not a crucial detail.

I provide 3 colors and 3 positions for 3 light sources to a Material. Shader uses just a single pass, and without any For loops or If statements, since we already know the number of light casters. But the biggest performance win is of course the Shadows: each point light would need an entire scene to be redrawn to get that shadow data.

Shader calculates lights as it normally would, but since its always only 3 lights, the code is more optimized.

Each light is then multiplied by the Baked Light Mask. I made it so that if mask value is below 0.5 - there is shadow, so all direct light and glossy reflection will be blocked. But light will still be rendered as Ambient light (as if it's coming from all directions, it will not be affected by normal, only by ambient occlusion mask). If it's a room, for example, the light will bounce from the ceiling and illuminate every corner behind every chair at least a little bit.

Texture Baked light:

Vertex Baked Shadows

Just like we can use RGBA texture to store shadow data, we can set Per-Vertex colors, which would contain exactly the same information. There are obvious problem that a triangle of a mesh will be either all shadowed or not at all, or shadowed on one side). So it is not a perfect method, sometimes you will need to add vertices, splitting faces, just to give shadow the shape it needs to have.


Thankfully my Playtime Painter does exactly that. But still, my initial tests show it to be upmost uncomfortable to add vertices just to create a shadow. Most will not consider the effort worth the result. I'll keep working on it, hope one day things will get much much more awesome) There is a third shadow Baking solution, which will probably be THE ONE:

Volume Baked Shadows

I made a plugin script for my Playtime Painter to edit Texture2D as if it were a volume:

Also in shader I had to write a special function which will translate world position into sampling UV coordinates. For a test I did setup some ugly lights. Result is, I could turn this:

into the image below. Mostly the shadows blocked the very bright blue light. For now script just ray-casts to see if there are shadows, but some gradations had to be painted manually. At some point there should be a ray-casting script for ambient:

To be fair I used terrible lights on purpose to accentuate the effect. They would have looked even more obnoxious without color bleed.

The main benefit of volumes is that they are independent from the geometry, so everything, including dynamic objects like Characters, particle effect, smoke and fog effects, will be able to display correct shadows.

I am planing to combine this with vertex baked shadows - for cases when you want a very smooth and nice shadows on the static geometry. Vertex shadow are also easy to calculate automatically.

Cons and Pros:

+ Shadows on dynamic objects

+ Glossy reflections

+ Light attenuation modified by Normal (unlike Baked light)

+ Mobile-friendly (unlike 3D Textures)

+ Performance - real-time shadows are expensive.

+ Ability to paint shadows

+ Ambient light (aside from shadow, intensity of the reflected light can be tuned)

+ Ambient occlusion for world light (in baked shadows Texture's alpha channel)

+ Lights can still change color or brightness

+ Lights have unlimited range (it still fades over distance, but a bright point will always be visible in glossy reflection)

- Each 3-4 lights will need another texture

- Light shouldn't move

- Sometimes shadows could be seen on the wrong side and sometimes be missing from shadowed wall because of volume resolution.

- Dynamic objects can't cast shadow

- Resolution is lower then a regular shadow.

- Texture size increases as the volume increases

[11.04.2018 update]

I realized that in most scenarios, if environment has high ambient occlusion (from sky light, which I paint into Alpha channel, as stated above) - it is likely to be an enclosed area, a room, or a corridor, or under trees in the wood, so at the same time this area is more likely to have a higher light bounce coefficient, because the light will bounce from walls and stuff. It is not always true, it could be a huge cave, in which case - no sky light, but plenty of room for light to scatter all around the place. Still, in most cases it seams to be helping me kill 2 birds with 2 stone: increasing bounce coefficient proportionally to to ambient occlusion.

[26.04.2018 update]

Aside from minor improvements, tried out to create a fog shader to receive volume shadows. Made shader fade away when viewed from an angle and when up close, so player can walk around and trough in the first perspective. Sampling volume twice, which results in 4 tex2D samplings: one is offset by view direction forward, other - backwards, which gives fog a bit of thickness feel.

Also, it is possible to use mip levels to save shadow calculation for Out Of Volume objects, bigger worldspace area, around the volume.

bottom of page