Problem: Instances moved every frame via UInstancedStaticMeshComponent smear/ghost badly under TAA/TSR. Motion-vector visualization shows the mesh, but with zero motion vectors — even though the instances are clearly moving.
Root cause (not an engine bug): per-instance velocity depends entirely on PerInstancePrevTransform being 1:1 with the instance count. If that array is empty, bHasPerInstanceDynamicData is false, and the velocity pass falls back to primitive-level LocalToWorld vs PreviousLocalToWorld. If your component doesn’t move (only the instances scroll), that comparison is equal → the primitive is treated as “not moving” → no motion vectors → ghosting.
The traps that make this hard
AddInstances()(plural) does not append toPerInstancePrevTransform— onlyAddInstance()(singular) does. (Confirmed bug; Epic reproduced it — see the thread linked at the bottom.)SetHasPerInstancePrevTransforms(true)must be called after instances exist, so it can seed prev from the current transforms.- If prev ends up empty, the flag silently drops back to
false(ISMInstanceUpdateChangeSet.cpp), with no log. - Prev transforms must be component-local (
bWorldSpace = false), not world space — otherwise the engine converts them with the current component transform while the shader multiplies by the previous primitive local-to-world.
Working fix (no engine modification)
// Setup
ISM->SetMobility(EComponentMobility::Movable);
// Build / rebuild (once, or when instance count changes)
void Rebuild(const TArray<FTransform>& CurrentLocal)
{
ISM->ClearInstances();
ISM->AddInstances(CurrentLocal, false, /*bWorldSpace*/ false, /*bUpdateNavigation*/ false);
ISM->SetHasPerInstancePrevTransforms(true); // must be AFTER AddInstances
checkf(ISM->PerInstancePrevTransform.Num() == ISM->GetInstanceCount(),
TEXT("prev %d vs instances %d"), ISM->PerInstancePrevTransform.Num(), ISM->GetInstanceCount());
}
// Every frame
void Update(const TArray<FTransform>& CurrentLocal, const TArray<FTransform>& PrevLocal)
{
ISM->BatchUpdateInstancesTransforms(
0, CurrentLocal, PrevLocal,
/*bWorldSpace*/ false, // see trap #4
/*bMarkRenderStateDirty*/ false, // TransformChanged already drives the upload
/*bTeleport*/ true); // no physics, skip the body path
}
Key points:
- Keep
PrevLocal= the transforms you sampled last frame (component-local), so the previous frame is real. - The
checkfcatches the silent-degrade trap during development.
How to verify (do all three)
- Log
PerInstancePrevTransform.Num() == GetInstanceCount()— you want1. r.Velocity.ForceOutput 0(it’s0by default) — if ghosting is gone with it off,bAlwaysHasVelocitycame from the flag, not the CVar.- Acid test: vehicle stationary + instances scrolling. Primitive-level velocity is zero here, so no ghosting = per-instance prev is genuinely working.
Tested on UE 5.5.4 (++UE5+Release-5.5).
Related threads
- AddInstances does not add per-instance previous transforms (Epic-confirmed bug)
- Instanced mesh rendering and motion/velocity vectors for TAA/TSR
- UE 5.0 Ghosting Issues with Instanced Static Meshes and TSR/Temporal AA
- Crash when accessing Previous Transforms in FISMCInstanceDataSceneProxy::ApplyChanges