Skip to content

Shadow Map Atlasing

RPaladin edited this page Dec 1, 2022 · 17 revisions

Shadow Map Atlasing

NOTE:

This feature is relatively new, and because of that, it's disabled by default to provide some stability for projects until it's adequately tested. While disabled, the code related to the feature is completely removed when compiling.

Sections


Introduction

  • What are shadow maps

Like most modern game engines, Armory employs shadow maps to draw shadows in a scene. This technique, simply put, it's the process of utilizing images, known as depth maps, to determine what it's lit and what is occluded when rendering the final image. It's a really efficient way to draw convincing shadows but it has a few different drawbacks and limitations.

  • Limited dynamic scenes

One of the problems with shadow maps is that it's difficult to optimize it for a considerable number of lights in a way that doesn't limit a scene. Newer versions of graphics API(s) offer tools to get around this problem at the cost of losing support of older targets.

So instead, an alternative is to use a solution called shadow map atlasing, which paired with clustered shading allows solving the original problem without updating.

  • What is shadow map atlasing and why it was chosen

Atlasing can be defined as the process of grouping multiple textures into one or several big textures. In this case, related shadow maps get grouped together into the same big texture or shadow map atlas.

This approach has a few benefits:

  • First and most importantly, it solves the limitation described earlier by simply getting around it, since for the shader part the atlas is just an UV calculation and a simple texture lookup.
  • Secondly, it may see a performance increase since the number of texture switches it's considerably reduced, depending on the target.

Yet atlasing has a few flaws that are worth mentioning:

  • In order to determine the UV coordinates in the atlas a few GPU cycles have to be wasted. This gets worsen when dealing with point lights, which require custom cubemap look ups.
  • No wrapping textures. This complicates determining UV coordinates and computing PCF for point lights.
  • A complicated code solution may be needed to manage the atlases if it's expected the scene dynamic.

How to use

  • Enabling shadow map atlasing

Armory can handle shadow map atlases out-of-the-box for you by simply toggling the Shadow Map Atlasing option in Render Properties > Armory Render Path > Shadows.

how to enable the option

When the option is enabled, Armory will automatically take care of the management of lights that affect the active scene and consequently their shadow maps.

Caveats

To keep scenes dynamic, there are a few caveats that you need to be aware when designing your scene light placement.


Configuration

Activating it will make available a few different options to better optimize it for the needs of your project and export target, which are explained below:

  • Max Lights

This sets the limit of how many visible lights will be rendered at the same time.

This value is used for the uniform count related to lights, so it's worth to keep this in mind when targeting devices with a low number of uniform count, like some mobile devices.

  • Max Lights Shadows

This sets the limit of how many shadow maps will be rendered at the same time. The same than Max Lights applies to this.

  • Shadow Map Atlas Single Map

Group all lights into one single atlas.

This reduces texture look ups and reduces slightly the overhead of the shadow map handling at the cost of losing overall space for many lights. Ideal if you don't plan to have a lot of lights in your scene and want to save as much performance as possible, while also keeping atlasing enabled.

  • Max Atlas Texture Size - type

These values define the max size of atlases.

It's important to set this limit properly, because if the limit goes beyond the texture size limit for the platform target, it will crash the game.

What limit to set

This depends on the project needs, but the limit should be just enough for the "worst case scenario", which depends on the light placement on the scene. To understand what is this "worst case scenario", first we need to understand a little bit of what goes on behind the scenes:

Tiles

The atlas is subdivided in what is known as tiles. tiles have the size of the shadow map size (cascade size for spot and sun, cubemap size for point).

Simplified visual representation of an atlas

Handling of Tiles

Free tiles will be assigned to lights whenever needed if there are enough available. If not enough could be assigned, then the light will not have shadows when rendering.

Under certain conditions a light will relinquish their used tiles, which allows reusing them for other lights. If the opposite conditions are met, the light will be added again to the atlas.

The conditions are as follow: the light is visible (data.raw.visible), it has a strength (data.raw.strength) > 0.0, it can cast shadows (data.raw.cast_shadow) and is visible to the camera (this is explained how it works later on).

The tiles for a light vary depending of the type of the light and settings:

Spot lights Point Lights Sun
tiles required 1 6 Cascades

The shadow map visualization in the debug console can help in understanding what goes on and what is the current shadow map size, which will grow according to scene needs.

Enabling LOD can help with reducing the space required for lights, but it depends on the distribution of lights in the scene and the strength of the lights, which can be inspected on the debug console as well.

  • Shadow Map Atlas LOD

Enable Level Of Detail for shadow maps, which dynamically changes the size of the required shadow map based on the light's distance in the camera view frustum (which is governed by the far value), the radius and strength of it. This allows saving space and optimistically increase the performance by reducing the size of shadow maps for far away lights.

The default shadow map size (cascade size and cubemap size) is the biggest size, and the smallest size depends on the number of subdivisions, see LOD Subdivisions. The highest detail is located at near plane distance of the view frustum (offseted to the front a little because of the clustering algorithm) and it ends with 0 in the far plane.

Details worth mentioning:

  • If there is not enough space for a bigger shadow map size, a light will remain with the same shadow map size it currently has until the next check the next frame.

simplified visual representation of how LOD works for shadow maps:

Simplified visual representation of Frustum View LOD and Culling

Light Culling

Out of view lights are always culled and have a 0 shadow map size. light culling depends on the strength of the lights and the distance to the camera, and it's position in the camera view frustum. Culling of "out of view" lights depend mostly of their strength: The bigger the strength the farther from view it has to be to be culled. This has to do with how the clustering algorithm works.

Be aware that view culling is not occlusion culling. All lights that are inside the view culling are rendered, regardless of if they are completely visible to the camera and if they even affect the lighting of the scene at all. This is important to keep in mind because lights that are occluded still affect performance at the moment.

  • LOD Subdivisions

This option allows controlling the number of subdivisions of LOD, for better personalization of it. The ideal configuration depends on how far the far plane distance of the camera is, and/or if there are some noticeable pops in quality because of it. Be aware that the more subdivisions the more performance it may take to handle them, because of how the algorithm to find free tiles work.


Debugging

Culled / Visible

Shows whether a light is being rendered or not, by their status: culled / visible on the debug console.

Clone this wiki locally