Why VSM Exists — The Problem with Traditional Shadow MapsVSM 的存在原因 — 传统阴影贴图的问题

Traditional Cascaded Shadow Maps (CSM) have a resolution problem: there are only a few fixed cascades, each covering a large area. Nearby surfaces get decent resolution, but at cascade boundaries or for point lights the effective texel density is much lower than the rendered scene resolution. The result: shadow edge artifacts, aliasing, and light leaking.

传统级联阴影贴图(CSM)有分辨率问题:只有几个固定的级联,每个覆盖大片区域。近处表面分辨率还行,但在级联边界或点光源处,有效纹素密度远低于渲染场景分辨率。结果:阴影边缘锯齿、走样和漏光。

Virtual Shadow Maps solve this with a radically different approach: instead of a few fixed-resolution maps, every light has access to a single huge virtual shadow map (16384×16384 pixels per light!) whose pages are allocated on demand only where shadow data is actually needed. Pages that are off-screen, occluded, or too far away are never allocated. This gives uniformly high shadow resolution everywhere, at the cost of a more complex allocation system.

虚拟阴影贴图用截然不同的方法解决了这个问题:每个光源不是几张固定分辨率贴图,而是可以访问单张巨大的虚拟阴影贴图(每光源 16384×16384 像素!),其页面仅在实际需要阴影数据的地方按需分配。不在屏幕上、被遮挡或太远的页面永不分配。这在任何地方都能提供均匀的高分辨率阴影,代价是更复杂的分配系统。

Virtual Address Space — 16K² Per Light虚拟地址空间 — 每光源 16K²

The virtual address space is defined by constants in FVirtualShadowMap (VirtualShadowMapArray.h:64):

虚拟地址空间由 FVirtualShadowMapVirtualShadowMapArray.h:64)中的常量定义:

// FVirtualShadowMap constants (VirtualShadowMapArray.h:63)
class FVirtualShadowMap
{
    // PageSize * Level0DimPagesXY = virtual resolution
    // e.g. 128 * 128 = 16384 = 16K texels per dimension per light
    static constexpr uint32 PageSize = VSM_PAGE_SIZE;           // 128 texels per page
    static constexpr uint32 Level0DimPagesXY = VSM_LEVEL0_DIM_PAGES_XY; // 128 pages × 128 pages
    static constexpr uint32 VirtualMaxResolutionXY = VSM_VIRTUAL_MAX_RESOLUTION_XY; // 16384
    static constexpr uint32 MaxMipLevels = VSM_MAX_MIP_LEVELS;  // 8 mip levels
};

// The virtual address space has 128×128 = 16384 pages at level 0
// Each page is 128×128 texels
// Total virtual resolution = 16384×16384 per light
// But: most of these pages are NEVER allocated — only ~1-5% are active

For directional lights, VSM uses a clipmap structure: multiple virtual maps at different resolutions, centered on the camera, cover near-to-far distances with decreasing texel density. This is similar to CSM but with the page-on-demand optimization.

对于方向光,VSM 使用裁剪贴图结构:以相机为中心的不同分辨率的多张虚拟贴图,以递减的纹素密度覆盖从近到远的距离。这类似于 CSM,但具有按需分配页面的优化。

Physical Page Pool — The GPU Memory Budget物理页面池 — GPU 内存预算

All physical shadow texels live in a single large 2D physical pool texture. Its size is controlled by r.Shadow.Virtual.MaxPhysicalPages (default: ~4096 pages for console quality). The pool is shared across ALL lights — this is the key memory saving: instead of one shadow map per light, all lights share a unified pool.

所有物理阴影纹素存在于单张大型 2D 物理池纹理中。其大小由 r.Shadow.Virtual.MaxPhysicalPages 控制(主机质量默认约 4096 页)。池在所有光源间共享——这是关键的内存节省:不是每个光源一张阴影贴图,所有光源共享一个统一的池。

Physical Pool Texture (e.g., 4096×4096 texels total): ┌───────────┬───────────┬───────────┬───────────┐ │ Sun page │ Sun page │ SpotLight │ SpotLight │ ← 128×128 texel pages │ (0,0) │ (1,0) │ page │ page │ ├───────────┼───────────┼───────────┼───────────┤ │ Sun page │ (unused) │ PointLight│ (unused) │ │ (0,1) │ │ page │ │ └───────────┴───────────┴───────────┴───────────┘ ↑ 32 pages × 32 pages = 1024 physical pages in this example

Page Table Indirection — Virtual to Physical Mapping页表间接 — 虚拟到物理映射

The page table is a 2D GPU texture that maps virtual page coordinates to physical page coordinates. For each light and each virtual page address, the page table stores either the physical page index (if the page is allocated) or a sentinel value (0xFFFFFFFF = not allocated). The shadow projection shader samples the page table first, then uses the result to sample the physical depth pool.

页表是一张 2D GPU 纹理,将虚拟页面坐标映射到物理页面坐标。对于每个光源和每个虚拟页面地址,页表存储物理页面索引(如果页面已分配)或哨兵值(0xFFFFFFFF = 未分配)。阴影投影着色器首先采样页表,然后使用结果采样物理深度池。

// Simplified shadow lookup in projection shader:
// (VirtualShadowMapProjectionCommon.ush)

float SampleVSMShadow(uint VirtualShadowMapId, float3 WorldPos)
{
    // 1. Project world position into light-space (gets virtual page + texel coords)
    float2 ShadowUV = ProjectToShadowSpace(VirtualShadowMapId, WorldPos);
    uint2 VirtualPageCoord = GetVirtualPageCoord(ShadowUV);
    
    // 2. Lookup page table: virtual → physical
    uint PageTableEntry = PageTable.Load(int3(VirtualPageCoord, 0));
    
    if (PageTableEntry == 0xFFFFFFFF)
        return 1.0f; // page not allocated = assume lit (no shadow data)
    
    // 3. Compute physical texel coordinate from page table entry
    uint2 PhysicalTexel = GetPhysicalTexelFromPageEntry(PageTableEntry, ShadowUV);
    
    // 4. Sample the physical depth pool
    float ShadowDepth = PhysicalShadowPool.Load(int3(PhysicalTexel, 0)).r;
    
    return (WorldDepth > ShadowDepth) ? 0.0f : 1.0f; // in shadow?
}

Page Marking & Allocation — GPU-Driven页面标记与分配 — GPU 驱动

Page allocation is a multi-stage GPU-driven process each frame:

页面分配是每帧的多阶段 GPU 驱动过程:

Cache Manager — Static vs Dynamic Split缓存管理器 — 静态与动态分离

The FVirtualShadowMapCacheManager tracks page validity across frames. It splits geometry into two categories:

FVirtualShadowMapCacheManager 跨帧跟踪页面有效性。它将几何体分为两类:

Static Pages静态页面Dynamic Pages动态页面
Geometry几何体Static meshes that never move从不移动的静态网格Skeletal meshes, Nanite (which can stream), moving objects骨骼网格、Nanite(可以流送)、移动对象
Caching缓存Cached indefinitely; only invalidated when geometry or light changes无限期缓存;仅在几何体或光源改变时失效Cached for one frame; must re-render if receiver moved into page缓存一帧;若接收体移入页面则必须重新渲染
Cost成本Near-zero after first render (cache hit)首次渲染后近乎为零(缓存命中)Re-rendered every frame for active pages每帧重新渲染活跃页面

Nanite Integration — One-Pass Shadow RenderingNanite 集成 — 单通道阴影渲染

VSM is designed to work with Nanite's cluster hierarchy. When rendering shadow pages for Nanite geometry, the culling shader can evaluate all visible pages across all lights in a single GPU pass by treating each light's virtual pages as additional views. The cluster LOD selection is the same algorithm as the main view but with adjusted screen-space error thresholds for the shadow resolution.

VSM 被设计为与 Nanite 的 Cluster 层级协同工作。为 Nanite 几何体渲染阴影页面时,剔除着色器可以通过将每个光源的虚拟页面视为额外视图,在单个 GPU 通道中评估所有光源的所有可见页面。Cluster LOD 选择使用与主视图相同的算法,但根据阴影分辨率调整了屏幕空间误差阈值。

Performance Bottlenecks & CVars性能瓶颈与控制台变量

VSM performance depends heavily on the number of dirty pages per frame. Common bottlenecks:

VSM 性能很大程度上取决于每帧脏页面的数量。常见瓶颈:

CVarRecommended range推荐范围Effect效果
r.Shadow.Virtual.MaxPhysicalPages2048–8192Total pool size. Higher = less eviction, more VRAM总池大小。越高 = 越少驱逐,更多 VRAM
r.Shadow.Virtual.ResolutionLodBiasLocal-2 to 0Negative = lower res for local lights (save pages)负值 = 局部光源分辨率更低(节省页面)
r.Shadow.Virtual.SMRT.RayCountLocal4–16Ray count for SMRT soft shadows on local lights局部光源 SMRT 软阴影的光线数

Source Navigation源码导航

Unreal Engine 5.6 · Engine/Source/Runtime/Renderer/Private/VirtualShadowMaps/
Source analysis · For learning purposes