Virtual Shadow Maps
- Consistent high-resolution shadowing for film-quality assets and large dynamic open worlds with Nanite, Lumen, World Partition.
- Goals: resolution matching Nanite; plausible soft shadows at controllable cost; works by default; replace many Stationary Light techniques with one unified path.
- "Conceptually, virtual shadow maps are just very high-resolution shadow maps." Virtual 16k x 16k; clipmaps for directional lights; pages 128x128 "allocated and rendered only as needed to shade on-screen pixels based on an analysis of the depth buffer"; cached between frames unless invalidated.
- Nanite doesn't support pre-shadows / per-object shadows; with Nanite use Movable lights or VSM.
- New projects default; r.Shadow.Virtual.Enable. "designed to be used with Nanite ... If your target platform or project does not use Nanite, Virtual Shadow Maps is likely not a good fit."
- Replaces stationary precomputed shadows, pre-shadows, per-object shadows, CSMs, movable local light shadows. Distance Field Shadows not replaced (take over for non-Nanite beyond Dynamic Shadow Distance). Nanite always renders to VSM regardless of distance. Contact shadows no longer necessary. Ray-traced shadows take precedence.
- SMRT: rays toward light; samples along ray tested against VSM; contact hardening; "Past methods based on Percentage-Closer Filtering (PCF) would over-blur". Source Radius / Source Angle. 8 rays at Epic; RayCountLocal/Directional (0 = hard); SamplesPerRay 4-8. Penumbra clamped; single depth layer -> light leaks handled by gap-filling heuristic.
- Directional clipmaps: each level own 16K VSM covering twice the previous radius; default levels 6-22 = 64 cm to ~40 km; empty levels insignificant. ResolutionLodBiasDirectional (-1 doubles resolution). Projective aliasing still possible.
- Spot: single 16k VSM with mip chain; Point: cube of 16k VSMs. Mip chosen by projecting screen pixel size into shadow space.
- Caching: light movement invalidates all its pages; casters moving/added/removed invalidate overlapping pages (bounds from light's perspective); WPO/PDO; skeletal deformation invalidates every frame. Cached Page visualization (green/red). Tight bounds. Invalidations fully in shadow of Nanite objects can be skipped (not for non-Nanite). Shadow Cache Invalidation Behavior: Auto/Always/Rigid/Static. WPO Disable Distance.
- Separate static caching: two depth copies (static/dynamic); invalidated static pages should be near 0.
- Moving light LOD bias; Coarse Pages for volumetric fog / forward translucency (can cause draw-call bottleneck for non-Nanite).
- Profiling: GPU Profiler; Insights trace.enable counters,vsm; CsvCategory VSM enable. Buckets: Shadow Depths and Shadow Projection. "Disabling asynchronous compute with r.RDG.AsyncCompute 0 will give you a more representative view" (profiling only). stat gpu timings can be unreliable, especially if CPU-bound.
- Shadow Depths: RenderVirtualShadowMaps(Nanite) (one pass for all directional, one for local) vs (Non-Nanite) per light with draw calls; "Non-Nanite geometry is much more expensive to render into VSMs" -> enable Nanite on all supported geometry incl. low-poly. Stats: r.ShaderPrintEnable 1, r.Shadow.Virtual.Stats 1. Non-Nanite needs full LODs; r.Shadow.RadiusThreshold; one instance can become 48 culling instances per point light (8 mips x 6 faces), 17 per directional.
- Shadow Projection: function of total samples (lights per pixel, rays per pixel, samples per ray); reduce Source Radius/Angle first; easier to control than depths.
- Platforms: PS5, Xbox Series, DX12 SM6.6 atomics or Vulkan int64 atomics; NVIDIA Maxwell+, AMD GCN+; Apple M2+.
- Limitations: tight bounds; many small local lights WIP; shadow terminator on low poly (add polygons; else r.Shadow.Virtual.NormalBias, default 0.5); physical page pool overflow -> corruption (r.Shadow.Virtual.MaxPhysicalPages); Subsurface Profile unsupported; "There is no simple solution to solving projective aliasing with shadow maps."