Summary
The instanced-skinning animation update (NaniteSkinningUpdateViewData.usf) only marks instances as deforming for chunks it reaches through the scene-culling cell hierarchy.
Chunks that exist outside that path are never emitted by pass 1, so their instances never receive bone transforms and render the skeleton’s reference pose — permanently and silently, while still drawing (Nanite’s draw path reads the same chunk record but ignores the skinned flag, so geometry renders fine).
Two manifestations:
-
Deterministic: the “uncullable” chunk range (primitives classified UnCullable, or everything when r.SceneCulling.TreatInstancedDynamicAsUnCullable=1) belongs to no cell, and ComputeExplicitChunkBounds only writes records for chunks reached from a cell (SceneCulling.cpp, DirtyCellBoundsIndices expansion ~L2531-2550). Its ExplicitChunkCellIds/ExplicitChunkBounds entries are never written, so NaniteSkinningUpdateChunkCullCS reads garbage and skips them (usf L46-63).
-
Organic: with a dense, continuously spawning crowd on one UInstancedSkinnedMeshComponent (~200 instances packed into a few cells, instance ranges reallocating), chunks progressively stop being emitted at 64-instance granularity until zero instances are marked. Verified by reading back ViewData.DeformingInstanceViewMask: after a few minutes zero bits are set while the CPU-side hierarchy passes both r.SceneCulling.ValidateAllInstanceAllocations and r.SceneCulling.ValidateGPUData (the affected buffers are not among the four that validation covers). A one-line experiment that emits every used chunk in pass 1 (letting pass 2’s per-instance checks decide) eliminates the issue in A/B/A testing, confirming pass 1’s record-based cull as the failing stage.
5.8.1 (5.8.1-56057345)
What Type of Bug are you experiencing?
AI
Steps to Reproduce
- Create a level with a UInstancedSkinnedMeshComponent playing an animation (e.g. via Anim Bank) and add a few hundred instances near the camera.
- Set r.SceneCulling.TreatInstancedDynamicAsUnCullable 1 (routes dynamic instanced primitives into the uncullable chunk range).
- PIE or -game. Observe the instances.
- (Organic variant, no cvar:) continuously spawn/remove instances so several hundred pack into a small area; within ~1-3 minutes of real time, instances stop animating in groups of 64 until none animate.
Expected Result
Instanced skinned mesh instances are marked for animation regardless of which chunk storage path (cell hierarchy or uncullable range) their primitive is routed through; instances visible on screen always receive bone transforms.
Observed Result
With the cvar set: every instance renders the skeleton’s reference pose from the first frame, permanently, while geometry draws normally. No warning, ensure, or log output.
In the organic dense-crowd case the same state is reached progressively (64-instance chunks at a time) and never recovers; a full render-state recreate re-enters the broken state within seconds.
Affects Versions
5.8
Platform(s)
Windows
Upload an image
Additional Notes
WORKAROUND (verified A/B/A + 9-minute soak with a GPU readback of the deforming mask):
The failure is entirely in pass 1’s chunk-level pre-cull; pass 2 of the same shader already re-validates every instance individually (ValidInstance, HIDDEN flag, Nanite residency, the primitive’s SKINNED_MESH flag, and the per-instance AnimationMinScreenSize screen-size test).
Pass 1’s cull is therefore only an optimization, and bypassing it is correctness-safe: no instance can be wrongly animated, because pass 2 rejects everything pass 1 was supposed to filter.
Edit Engine/Shaders/Private/Nanite/NaniteSkinningUpdateViewData.usf, in
NaniteSkinningUpdateChunkCullCS, immediately AFTER the IsChunkUsed early-out (before LoadPackedExplicitChunkBounds is read):
// Workaround: emit every used chunk unconditionally instead of trusting the per-chunk
// attribute record (ExplicitChunkBounds / ExplicitChunkCellIds), which is never written
// for the uncullable range and can be stale for freshly reallocated chunk ids.
// Pass 2 re-validates every instance from GPUScene, so this cannot over-animate.
EmitChunk(ChunkId, (1u << NumSceneRendererPrimaryViews) - 1u);
return;
Notes for anyone applying this:
- Shader-only change: no engine rebuild needed. The next editor/game launch detects the source change and recompiles the two affected global shaders automatically.
- Cost: pass 2 now runs one 64-thread group per USED chunk in the scene instead of per surviving chunk (roughly one group per 64 GPU-Scene instances). Each rejected thread is a few buffer reads and an early-out; in our scenes (a few thousand instances) the difference is not measurable. What is actually lost is only the chunk-granularity screen-size early-cull for skinned meshes - the per-instance screen-size cutoff still applies in pass 2, so distant instances still stop animating exactly where AnimationMinScreenSize says they should.
- The bypassed record is also read by SceneCullingDebugRender and by Nanite’s instance hierarchy culling for DRAWING (AABB and draw distances only, not the skinned flag) - this edit does not touch those paths; it only stops the ANIMATION pass from trusting the record.


