Virtual Shadow Maps have been the default directional shadow technique in Unreal since 5.0, but the number of projects that actually ship them well is embarrassingly small. Most teams stand up a World Partition project, enable VSMs, see a 9 ms shadow pass on their target console, and start digging through the forums for how to turn them back into CSMs. That path is a dead end now. As of UE5.7, non-Nanite meshes have dramatically better caching behavior, the page pool is more gracefully resizable, and several of the old footguns around WPO and foliage have been quietly sanded down. If you spent 2024 and early 2025 avoiding VSMs, this is a reasonable moment to re-evaluate.
This post is the document I wish I had two years ago: what VSMs actually cost, where the costs come from, and how to tune a 4–16 km² open world so shadow rendering fits inside a realistic budget on each platform we care about. Every number here is from a single 2026-era test scene running UE5.7.1 — a 9 km² heightmap with World Partition, roughly 180k foliage instances per loaded cell, a few thousand Nanite meshes, and a directional sun with three clipmaps plus a couple of local light VSMs. Your numbers will differ. The shape of the tradeoffs will not.
How VSMs actually work, in just enough detail
Skip this section if you already know it. If you have ever tuned r.Shadow.Virtual.* without being able to predict what would happen, read it.
A Virtual Shadow Map is a 16k × 16k conceptual shadow map per light, subdivided into 128 × 128 pixel pages. Only the pages that are actually visible from the main view get rendered, and only when their contents are invalid. Those pages live in a shared page pool — a single texture that every VSM-enabled light draws from. The page pool is the resource you are actually budgeting.
Three things can invalidate a page:
- The camera moves such that the page now needs a different LOD (clipmap transition).
- A primitive inside the page changed — moved, animated, or had its WPO update.
- A light moved, which invalidates every page for that light.
The first is unavoidable and cheap. The third you simply do not do in gameplay. The second is where every real open-world perf problem lives. A single bush with WPO wind on every frame, multiplied by ten thousand instances scattered across your landscape, will happily eat 6 ms of shadow render time on a PS5 and you will have no idea why.
Cached pages are effectively free. Invalidated pages cost roughly what they would cost in a traditional shadow map pass, plus the bookkeeping. The entire optimization game is: keep pages cached, shrink the pool to what you actually need, and make sure the things that do invalidate are cheap to re-render.
The baseline numbers
Here is the cold-start cost on the test scene, standing in a dense forest cell at 1080p internal, TSR to target resolution, everything at Epic scalability:
| Platform | VSM total | Page alloc | Render | Project | Notes |
|---|---|---|---|---|---|
| PC, RTX 4070 | 2.1 ms | 0.3 ms | 1.2 ms | 0.6 ms | Baseline |
| PC, RTX 3060 | 3.4 ms | 0.4 ms | 2.1 ms | 0.9 ms | Baseline |
| PS5 | 3.1 ms | 0.3 ms | 2.0 ms | 0.8 ms | 60 Hz target |
| Xbox Series X | 3.3 ms | 0.3 ms | 2.2 ms | 0.8 ms | 60 Hz target |
| Xbox Series S | 6.9 ms | 0.4 ms | 4.8 ms | 1.7 ms | 30 Hz target |
| Steam Deck | 11.4 ms | 0.5 ms | 8.2 ms | 2.7 ms | 30 Hz target |
Those are "standing still in a clean scene" numbers. They are not the numbers you will see when combat starts. The point of this table is that the Steam Deck and Series S are a completely different conversation from the other platforms, and you should plan for that from the first prototype.
The moment you start moving the camera through foliage that has WPO enabled, every one of those numbers doubles. That is what we are going to fix.
Page pool sizing
The single most common VSM mistake we see on contract work is an undersized page pool with aggressive resolution biasing to compensate. This produces a game that looks perpetually soft and runs badly. The second most common mistake is a page pool so oversized that it drops into the paging path on handhelds and tanks.
r.Shadow.Virtual.MaxPhysicalPages controls pool size in pages. Each page is 128 × 128 R32 depth, which is 64 KB. So 4096 pages is 256 MB, 8192 pages is 512 MB, and so on. Useful reference points:
- 4096 pages (256 MB): Steam Deck ceiling. Pushes hard.
- 6144 pages (384 MB): Series S realistic budget.
- 8192 pages (512 MB): PS5/Series X default. Fine.
- 12288 pages (768 MB): PC mid-range. Headroom for bigger clipmaps.
- 16384+ pages (1 GB+): PC high-end only. Watch VRAM.
The project's default of 4096 is fine for linear games and completely wrong for open worlds. The symptom of an under-sized pool is a warning in stat VirtualShadowMapCache that says "Physical page pool overflow" or a visible stippling pattern in shadows when the camera rotates quickly. The fix is almost always to raise the pool, not to lower resolution.
We ship these three as per-platform DeviceProfiles:
[/Script/Engine.RendererSettings]
r.Shadow.Virtual.MaxPhysicalPages=8192
r.Shadow.Virtual.ResolutionLodBiasDirectional=0
r.Shadow.Virtual.ResolutionLodBiasLocal=0
; SteamDeck
r.Shadow.Virtual.MaxPhysicalPages=4096
r.Shadow.Virtual.ResolutionLodBiasDirectional=1.0
r.Shadow.Virtual.ResolutionLodBiasLocal=1.5
; XSX_PS5
r.Shadow.Virtual.MaxPhysicalPages=8192
Do not set these per-level or per-streaming-cell. VSM pool reallocation is not free and can stall for a frame or two.
Clipmap tuning for directional light
Directional light VSMs are a stack of clipmaps. Each clipmap level represents a shell around the camera at a fixed world-space resolution. The first level is densest; each subsequent level halves the sampling rate and doubles the radius. UE5.7 uses 6 levels by default, which is overkill for almost every open-world game we have measured.
Two settings matter:
r.Shadow.Virtual.Clipmap.FirstLevel— the finest clipmap. Default 6 (~64 world units per pixel at the first level, depending on radius formula).r.Shadow.Virtual.Clipmap.LastLevel— the coarsest. Default 22 (~16 km reach).
For a game with a 4 km visible horizon, LastLevel=20 saves roughly 0.4–0.6 ms on consoles by cutting the two outer shells that were only rendering distant mountains. If you also crank the fog to occlude the horizon, the visual difference is imperceptible.
For handhelds, we also push FirstLevel up by 1. This halves the resolution of shadows within the first ~16 m of the camera. It is visible on close grass and invisible on everything else, and it saves 1.2 ms on the Deck.
A trick that seems to be underused: r.Shadow.Virtual.Clipmap.ResolutionLodBiasDirectionalMoving. When the camera is moving faster than a threshold, this automatically biases resolution. The invalidation cost of clipmap shifts is paid either way — you may as well render the new pages cheaper. We set this to 0.5 on PC and 1.0 on consoles and have never heard a complaint.
Invalidation, WPO, and the foliage problem
Every blade of grass with wind WPO invalidates its VSM page every frame. That is the problem. There are four reasonable paths out of it, and you probably want a combination.
1. Disable WPO for shadows on small foliage
On the foliage's static mesh or foliage type, there is an "Evaluate World Position Offset" flag for shadows. Turn it off for anything smaller than waist-high. Players do not notice that grass shadows are not waving. They will notice a 4 ms hit.
On our test scene, this single change dropped forest-floor shadow cost from 8.2 ms to 3.1 ms on PS5. It is the highest-leverage VSM optimization you can make, and it is one checkbox.
2. Use WPO disable distance
For foliage where shadow-WPO does matter (larger plants, hanging banners, cloth), set a WPO disable distance. The engine will stop evaluating WPO past that range, which also stops invalidating those pages. We use 30 m for mid-size plants, 80 m for trees, effectively never for hero props. UE5.7 respects this for shadow passes specifically, which was not reliable in 5.3.
3. Procedural placement with invalidation-aware LODs
This is where Procedural Placement Tool earns its keep on open-world projects. It ships with an "invalidation budget" mode where density scatters respect a per-material WPO cost hint, and the tool automatically shifts far-LOD instances to variants without WPO enabled. You end up with the same visual density, but only the instances within a tunable radius actually participate in shadow invalidation. On our 9 km² test scene, that shifted 72% of foliage instances out of the per-frame invalidation pool without touching the art.
If you are building scatters by hand in the foliage tool, you can simulate this with per-instance custom data and a material switch, but you are writing code either way.
4. Avoid animated mesh hierarchies in shadow range
Skeletal meshes are the other big invalidator. A skeletal mesh casting a VSM shadow invalidates pages every frame the animation updates. For crowd NPCs, this is unavoidable; for decorative animated props (windmills, waterwheels, flags), consider whether they need to cast VSM shadows at all. Turning off shadow cast on decorative animated props saved 0.9 ms in our village scene.
Non-Nanite, Nanite, and the Page Render path
UE5.7 finally made non-Nanite mesh shadow rendering into the VSM pool competitive with the old CSM path. Prior to 5.7, the recommendation was "Nanite-ify everything or live with the cost." That is no longer strictly necessary. But Nanite meshes still have a meaningful advantage in the shadow pass: they stream their own LODs, so distant clipmap levels render far less geometry automatically.
Our rule of thumb:
- Nanite: All static world geometry, rocks, architecture, terrain meshes, hero foliage trunks.
- Non-Nanite: Small foliage (leaves, grass), skeletal meshes, vehicles, anything with deformable/physics behavior.
- Do not Nanite-ify: Meshes under ~500 triangles. The Nanite overhead exceeds the benefit.
The 5.7 non-Nanite VSM render is about 1.6× the cost of the Nanite path per invalidated page on our scene. Workable, but not free.
Local light VSMs
Everything above is about the directional sun. Local lights (point, spot, rect) also use VSMs, and they follow mostly the same rules with one important caveat: local VSMs have no clipmap structure. They are a single 16k virtual map. That means they are extremely cheap when cached and extremely expensive to invalidate if the light moves.
Rules we enforce:
- Static local lights with VSMs: great, cheap, do many.
- Moving local lights with VSMs: budget one or two per frame. Consider Distance Field shadows or stationary shadow maps instead.
- Gameplay-driven flickering or animated lights: never VSM.
On PS5, each fully invalidated local VSM costs roughly 0.4–0.8 ms depending on geometric complexity in range. Cached local VSMs cost about 0.05 ms each, so you can have dozens with no issue.
Platform-specific tuning
PS5 and Xbox Series X
These are the easy platforms. The defaults mostly work. The tuning that matters:
r.Shadow.Virtual.MaxPhysicalPages=8192r.Shadow.Virtual.Clipmap.LastLevel=20(if you have horizon fog)- WPO disable on small foliage shadows
- Use async compute for the VSM build pass —
r.Shadow.Virtual.UseAsync=1. This hides about 0.4 ms behind the base pass on both platforms.
Target: 2.5–3.5 ms for shadows at 60 Hz, which is where you need to be to fit a full frame in 16.6 ms.
Xbox Series S
The Series S is where most VSM projects die. Half the memory bandwidth, half the CU count, same resolution expectations. Our approach:
MaxPhysicalPages=6144. 4096 is too tight for a real open world, 8192 thrashes.ResolutionLodBiasDirectional=0.5. Noticeable only if you are comparing side-by-side.- Render resolution ceiling at 1440p internal. TSR to 4K. The VSM pool cost scales with visible pages, which scales with resolution.
- 30 Hz target, not 60. If you are trying to hit 60 on Series S with a real open world and VSMs, you are going to lose.
Series S shadow budget at 30 Hz: 5–7 ms. Tight but feasible.
Steam Deck
The Deck has a 1280 × 800 native panel. That is an enormous advantage for VSM cost. Your real constraint is the 15 W TDP and the tiny page pool you can afford.
MaxPhysicalPages=4096ResolutionLodBiasDirectional=1.0Clipmap.FirstLevel=7(one notch coarser than default)Clipmap.LastLevel=19- Shadow distance cap at 2 km maximum. Past that, you are rendering pages for shadows nobody can see on an 800p panel.
- Aggressive foliage WPO disable radius. 15 m, not 30.
Target: 6–9 ms at 30 Hz. This is a real budget, but you can hit it.
PC, broadly
PC is the platform where you need scalability, not tuning. Our PC scalability:
- Low:
MaxPhysicalPages=4096,ResolutionLodBiasDirectional=1.0,LastLevel=18. - Medium:
MaxPhysicalPages=6144,ResolutionLodBiasDirectional=0.5,LastLevel=20. - High:
MaxPhysicalPages=8192, defaults,LastLevel=22. - Epic:
MaxPhysicalPages=12288, defaults,LastLevel=22,r.Shadow.Virtual.SMRT.RayCountDirectional=8.
Do not let users raise MaxPhysicalPages above 16384 via console. VRAM fragmentation can become a problem on 12 GB cards when the pool reallocates mid-session.
Cache invalidation debugging
When shadow cost mysteriously spikes, you are looking at an invalidation storm. The tools for finding them are scattered; here is the workflow.
stat VirtualShadowMapCache. Look atInvalidated Pagesper frame. Anything over 5–10% of total used pages is a storm.r.Shadow.Virtual.Cache.DrawInvalidatingBounds 1. Draws the world-space bounds of every invalidating primitive in green. The forest floor will light up like a Christmas tree the first time you do this.r.Shadow.Virtual.Visualize 1with mode 2 (Page Allocation). Red pages are newly allocated this frame, green are cached. A wall of red means your cache is not working.- For a specific primitive you suspect, check its "Cast VSM Shadow" and "Affect VSM Shadows" settings. UE5.7 splits these from the general cast-shadow flag; prior versions did not.
Storm causes we have seen in shipping projects:
- A movable light accidentally marked stationary, invalidating on every tick.
- Blueprint-driven foliage instance updates running every frame.
- Skeletal mesh particles casting shadows at distance.
- Water plane with animated material having shadow casting on. Water does not need to cast shadows. Turn it off.
Common mistakes, in order of how often we see them
- Leaving WPO on for small foliage shadows. Described above. The single biggest win.
- Undersized page pool with resolution bias to compensate. Raise the pool. Stop biasing.
- Never calling
r.Shadow.Virtual.Cache.MaxFramesSinceLastUsed. Default is 60. If your cells stream in and out rapidly, lowering this to 20–30 reclaims pool space faster and avoids overflow at seams. - Treating the Series S like a PS5. It is not. It needs its own profile from day one.
- Running VSMs with Lumen Hardware RT at Epic on Series S. Pick one. You cannot have both.
- Ignoring
r.Shadow.Virtual.UseAsync. Free 0.4 ms on consoles. Turn it on. - Shadow cast on VFX. Niagara emitters almost never need to cast VSM shadows. Audit every system.
- Nanite-ifying a forest of 300-tri leaf cards. The overhead eats the benefit.
- Not using streaming proxies for distant foliage. Past 150 m, a single large mesh with painted-on foliage textures costs a fraction of actual instances. Procedural Placement Tool can bake these automatically per streaming cell.
- Relying on Unreal Insights captures from an editor PIE session to budget for console. PIE is a lie. Capture on device or do not bother.
The 5.7 regressions worth knowing about
Two things got worse in 5.7 that you should be aware of:
- Water material invalidation. The new water rendering path in 5.7 can cause VSM invalidation on the water plane even when shadow casting is disabled. Workaround: set the water material's "Render in Main Pass" to off for shadow casting specifically. Epic acknowledged this on the public tracker; fix expected in 5.7.3.
- Virtual shadow map with Lumen Surface Cache on landscape. In specific heightmap configurations, you can get a cache coherency issue that looks like faint diagonal banding in shadows.
r.Lumen.SurfaceCache.UpdateFrequencyScale 0.5mitigates it without breaking Lumen.
Both are small. Neither is a reason to stay on 5.6.
A reasonable ship target
For an open-world game on current-gen consoles, budget:
- PS5/XSX, 60 Hz: 2.5–3.5 ms shadows
- XSX/PS5, 30 Hz: 4–5 ms shadows
- Series S, 30 Hz: 5–7 ms shadows
- Steam Deck, 30 Hz: 6–9 ms shadows
- PC high-end: 1.5–2.5 ms shadows
- PC low-end: whatever scalability produces
If you are outside those bands and have already applied everything in this post, the remaining work is content: measure which assets are producing the storms, and fix the assets. There is no magic cvar that will save a level full of wind-animated grass with shadow casting on.
The good news is that once the caching is working, VSMs are dramatically better than CSMs for the kind of dense, detailed worlds every project is shipping in 2026. You get stable shadows at all distances, correct contact shadows on characters, and no more cascade-seam popping. The cost of getting there is one week of serious profiling and one set of DeviceProfile configs. That is a price worth paying.
If you want a shortcut for the foliage portion specifically, Procedural Placement Tool handles the invalidation-aware scatter and far-LOD proxy baking out of the box. If you want to script the whole profile-and-tune loop from editor automation, Unreal MCP Server can drive console commands, capture Unreal Insights sessions, and diff stats across runs without leaving your chat client.
VSMs are not optional anymore. They are also not as hard as the forum consensus suggests. Tune the pool, kill the WPO storms, accept that the Series S needs its own life, and move on to the next bottleneck.