Skip to content

Virtual Shadow Maps

EEVEE uses shadow mapping as its primary shadow solution. This is compatible with all pipelines (deferred, forward and volume), offers good performance and allows caching.

Sun lights use either a clipmap (for high field of view) or a cascade of shadow maps (for shallow field of view).

Local lights use local space shadow cubemaps. This allows to skip faces of the cube that are not used by certain light types (area lights, spot lights).

Each shadow map is stored as a virtual textures. There is conceptually one virtual texture for each cubeface, clipmap level and cascade level. However, all of these virtual shadow maps have the same physical memory pool which is a single texture atlas (eevee::ShadowModule::atlas_tx_).

All the virtual texturing is custom implemented and does not rely on the driver own virtual texturing capabilities. The whole page allocation and update logic all runs on the GPU with no synchronization.

Layout

One virtual shadow map is composed of a descriptor ShadowTileMapData and a table of virtual page info ShadowTileData.

Here is how classes are referenced.

flowchart LR
    light["`***Light***`"]
    sun_shadow["`***ShadowDirectional***`"]
    local_shadow["`***ShadowPunctual***`"]
    sun_tilemap["`***ShadowTileMap***`"]
    sun_tilemap_data["`***ShadowTileMapData***`"]
    tile_pool["`***ShadowTileData***`"]
    altas["`Shadow Atlas`"]

    subgraph CPU
    light --> |directional| sun_shadow
    light --> |punctual| local_shadow
    sun_shadow --> |tilemaps_| sun_tilemap
    local_shadow --> |tilemaps_| sun_tilemap
    end

    sun_tilemap -.-> sun_tilemap_data

    subgraph GPU
    sun_tilemap_data --> |tiles_index| tile_pool --> |page| altas
    end

    classDef gpu_node_class stroke:#f66
    classDef cpu_node_class stroke:#66f

    class light,sun_shadow,local_shadow,sun_tilemap cpu_node_class;
    class sun_tilemap_data,tile_pool,altas gpu_node_class;

For faster access during rendering and shading, the ShadowTileMapData and ShadowTileData are converted into other data structure. This is done after all page allocation has run. The shaders responsible for this are eevee_shadow_tilemap_finalize_comp.glsl and eevee_shadow_tilemap_amend_comp.glsl.

flowchart LR
    light["`***LightData***`"]
    tile["`***ShadowSamplingTile***`"]
    altas["`Shadow Atlas`"]
    render_view["`Render View`"]
    rendermap["`Render Map`"]

    subgraph Shading
    light --> |tilemap_index| tile
    end

    subgraph Rendering
    render_view --> |view_index| rendermap
    end

    tile --> |page| altas
    rendermap --> |page| altas

    classDef gpu_node_class stroke:#f66

    class light,tile_atlas,tile,altas,render_view,rendermap gpu_node_class;

Page State

The atlas physical pages can be in 3 states: freed, unused and used. The page is not an object but just a location inside the atlas. The states are implicit from the page owner / location.

flowchart LR
    tile_data["`***ShadowTileData***`"]
    page_heap["`Free Page Buffer`"]
    page_cache["`Cached Page Buffer`"]

    tile_data --> |free| page_heap
    tile_data --> |unuse| page_cache
    page_cache --> |free| page_heap
    page_heap --> |allocate| tile_data
    page_cache --> |reuse| tile_data

All pages start inside the Free Page Buffer.

If a page is allocated then it becomes owned by a ShadowTileData (which imply used state).

If a page gets out of view and still contains valid data, it becomes unused and is put into Cached Page Buffer.

If the Free Page Buffer is not enough to satisfy the needed allocation, cached pages are forced to be free.

The entry inside the Cached Page Buffer contains a reference to the previous owner ShadowTileData. The ShadowTileData also contains a reference to the Cached Page Buffer entry. This allows for all operations to be done in O(1) time.

Execution

The shadow update loop runs for every frame for shading view (camera views or light probe capture views). It ensures all visible shadows are up to date, free unused pages, and allocate more resolution where needed.

flowchart LR
    subgraph Setup
      B[Tilemap Setup] --> C[Update Tagging] --> D[Usage Tagging] --> E[Tile Masking]
    end
    subgraph Allocation
      F[Page Free] --> G[Page Defrag] --> H[Page Allocate]
    end
    subgraph Rendering
      I[Create Render Views] --> L[Create Render Map]
      I --> J[Amend Shading Tilemap]
      I --> K[Page Clear]
      L & K --> M[Shadow Map Render]
    end  
    A[Directional Bounds] --> B
    E --> F
    H --> I

Directional Bounds

Directional shadows needs to have a near plane to render correctly. This pass iterate over all bounding boxes in the scene and output the minimum near plane for each directional light that fits the whole scene.

Setup

This passes handle ShadowTileData updates and tagging.

The Tilemap Setup resets the tile states (see init_tile_data()) and shifts the ShadowTileData for directional shadows. It also sets pages dirty (with SHADOW_DO_UPDATE) if there was a light update.

Then Update tagging loops over the bounding boxes of updated shadow casters and tag the intersecting tile for update.

Usage Tagging scan all shadow receivers and reproject them onto the virtual shadow maps, and tag the corresponding tiles as used. For Dithered material objects this is done by scanning the depth buffer. For volumes, this loads the volume properties and only tag if there is any scattering. For Blended material, we ray-march the bounding volume of the object and tag all tiles along the ray.

The previous page tagging can request a lot of redundant information. For instance, for a same location in the virtual shadow map, multiple level of detail can be tagged. The Tile Masking phase untag the lower LOD tiles that are completely overlaped by higher LOD tiles. This is also where update throttle happens.

Page allocation

Page allocation happens in 3 phases.

The Page Free phase frees unused pages, reclaim needed cached pages, and count the number of needed allocations.

Reclaiming the cached pages can leave the Cached Page Buffer in a non contiguous state, which is bad for the lockless insertion that Page Free also need to perform. That's why the Page Defrag phase defragments the buffer, leaving a contiguous array of cached pages.

If there are more allocations than free pages, the first pages inside the Cached Page Buffer are freed until there is enough free pages to satisfy all the allocations.

The Page Allocation phase simply pop the Free Page Buffer for all tiles marked as used but that have no physical page assigned.

We can run into the situation where there are more allocation needed than total available pages. In this case, the system output an error and the user needs to either reduce the amount of shadows or their precision in order to fix the issue. Note that this error is recoverable, it should never result in a crash. The resulting artifact of this situation is missing shadow tiles.

Shadow rendering

The Create Render Views pass scans all tilemaps, extract updated LOD regions and create a render view for each. Up to 64 render views can be created. Exceeding this limit can cause missing shadow or shadow lag artifacts until everything is updated.

The pass outputs: - a list of views to render and their associated data ShadowRenderView and ViewMatrices. - a Clear Tile Buffer that lists all tiles to be cleared to max depth. - a Tilemap Texture which contains ShadowSamplingTile, a different version of ShadowTileData that allows O(1) access to all level of details.

The Create Render Map pass create a Render Map that maps regions of the render views to the shadow atlas pages. Having this structure bypasses the need to go through the whole RenderView > Tilemap > TileData > Atlas Page indirection (reducing 3 level of indirection to 1). It is generated by a separate pass because to reduce the shader complexity as it caused some issues on some platform.

The Amend Shading Tilemap allows to propagate tiles indirections across tilemaps. This is useful for shadow clipmaps where the LODs are contained in different tilemaps. This ensures 0(1) lookup time for any LOD at any position. This is needed by shadow map tracing as the shadow map values can be queried outside of the requested position and these values usually exists in lower LODs.

For detail about the rendering itself, see Shadow Pipeline.

After rendering, the Shadow Atlas depths represents the distances from the shadow origin for punctuals and as distance from near plane for directionals. It is not the usual Z depths used in shadow mapping. This simplifies the shadow tracing algorithm and position reconstruction.

Update throttling

Updating a shadow map has a non negligeable cost. To keep the viewport and editing responsive, only a limited level of detail is updated per virtual shadow map for a single frame. We only update the lowest LOD first and progressively update the higher LODs if there is no updates.

Shadow Map Tracing

EEVEE computes shadows by doing ray casting from the light towards the shading point. The ray traversal uses the virtual shadow map to find intersection. This allows (to some extent) plausible soft shadows at a reasonable cost.

The ray generation picks a random point on the light shape as ray origin. This step is not physically based at all as we don't consider the BSDF distribution for choosing this direction. We ensure that the ray is not below the horizon and that the end of the ray is not in regions of the shadow map that are likely to not be available (because of tagging) as it can create invalid shadowing (and thus noise).

The ray end point is jittered around the shading point in a similar way as for PCF filtering. This allows to hide the remaining aliasing artifacts. Note that we use optimal biases here to avoid self shadowing, but this only works if the shading normal is close to the geometric normal.

The backward tracing allows to keep track of the latest potential occluders and use heuristics to pass behind occluders that are too far away from the ray.

To simplify the extrapolation of the last known occluder, we create a basis around the ray and the shadow map origin. This allows us to reduce the intersection test to a 2D problem. In this configuration, the ray is intersecting geometry only if the extrapolated occluder crosses the ray.

Shadow Map Jittering

This quality option randomize the position of the shadow map origin (or the angle, in the case of sun lights) for each render sample. The goal is to overcome the limitations of the single layer shadow map and the shadow map tracing. This technique is not physically based but it allows for very smooth shadows without aliasing. Note that this only jitter the shadow and not the shading.

For sphere light, we randomize the shadow center inside the ball with a special distribution that avoid biasing the shadow towards the center (see sample_ball()).

For sun light, we select a random point on the spherical cap sustented by the light and use it to setup our shadow. However, the lighting code will still use the original light direction.

This jittering is applied on the GPU, after the light culling phase. This is because we upload all GPU data after the sync phase, but this needs to amend the data that is alread for each render sample. The code for that is located in eevee_light_shadow_setup_comp.glsl.