【UE5】使用 InstancedStaticMesh 实现海量实例动态渲染的性能优化实践
目标
尝试使用 InstancedStaticMesh(ISM) 实现大量物体的实例化渲染,并在每帧 Tick 时修改每个实例的旋转值,测试在海量物体持续运动场景下的性能表现。
实现
在实现过程中,我最初使用的是 HierarchicalInstancedStaticMesh(HISM) 。
但实际测试发现,在频繁更新 Transform 的情况下,它的更新开销非常大。
如果没有层级裁剪等特殊需求,且实例需要大量、动态更新时,InstancedStaticMesh 的性能通常更有优势。
另外,使用 ISM 进行大规模每帧更新时,建议将CollisionEnabled设置为NoCollision,否则碰撞相关的更新会带来明显的额外性能消耗。我发现在 UE 中,一个常见的优化思路是:关闭不必要的功能,只保留真正需要的部分。
方案一:InstancedStaticMesh 配合蓝图实现实例更新
下面是 Tick 事件蓝图的关键逻辑:
![]()
在编辑器中运行游戏时,帧率表现并不理想。通过 Unreal Insights 分析发现,大部分时间都消耗在循环更新 Transform Array 的逻辑上。
![]()
但这部分本质上只是简单的数学计算。仅 10K 个实例循环更新 Transform 就产生了较高的耗时,因此基本可以判断,瓶颈主要来自蓝图自身的运行开销。
方案二:InstancedStaticMesh配合C++实现的实例更新
既然蓝图性能不足,于是改用 C++ 实现。
我编写了一个 AActor 子类,在 BeginPlay 中创建 InstancedStaticMesh 组件并挂载到根节点,然后在 Tick 中更新实例 Transform。
Tick部分的实现代码如下:
void ABatchedBoxes::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
const float AngleRad = FMath::DegreesToRadians(RotationSpeed * DeltaTime);
const FQuat DeltaQuat(FVector::UpVector, AngleRad);
const int32 Count = CachedTransforms.Num();
for (int32 i = 0; i < Count; ++i)
{
FTransform& T = CachedTransforms[i];
FQuat Q = T.GetRotation();
T.SetRotation(DeltaQuat * Q);
}
ISM->BatchUpdateInstancesTransforms(
0,
CachedTransforms,
false, // local space
true, // mark render dirty
false // teleport
);
}
C++ 版本的性能有明显提升。
但通过 Unreal Insights 进一步分析发现,Game Thread 中UInstancedStaticMeshComponent::CalcBoundsImpl 占用了较大比例时间,看起来是在计算组件整体包围盒。
![]()
在当前场景下,整体包围盒变化不大,因此这部分计算并非必要。
于是我对初始化逻辑进行了优化:
- 添加一个固定包围盒组件
- 将 ISM 的
bUseAttachParentBound设置为 true
这样可以复用父节点包围盒,避免每帧重新计算。
下面是构造函数与BeginPlay的逻辑实现:
// Sets default values
ABatchedBoxes::ABatchedBoxes()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
UBoxComponent* BoundsRoot = CreateDefaultSubobject<UBoxComponent>(TEXT("BoundsRoot"));
BoundsRoot->SetCollisionEnabled(ECollisionEnabled::NoCollision);
BoundsRoot->SetGenerateOverlapEvents(false);
BoundsRoot->SetCanEverAffectNavigation(false);
BoundsRoot->SetMobility(EComponentMobility::Movable);
RootComponent = BoundsRoot;
ISM = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("ISM"));
ISM->SetupAttachment(BoundsRoot);
ISM->bUseAttachParentBound = true;
ISM->SetCollisionEnabled(ECollisionEnabled::NoCollision);
ISM->SetGenerateOverlapEvents(false);
ISM->SetCanEverAffectNavigation(false);
ISM->bCastDynamicShadow = false;
ISM->bCastStaticShadow = false;
ISM->bAffectDistanceFieldLighting = false;
ISM->bReceivesDecals = false;
ISM->SetMobility(EComponentMobility::Movable);
ISM->NumCustomDataFloats = 0;
}
// Called when the game starts or when spawned
void ABatchedBoxes::BeginPlay()
{
Super::BeginPlay();
if (InstanceStaticMesh)
{
ISM->SetStaticMesh(InstanceStaticMesh);
}
const int32 Grid = FMath::CeilToInt(FMath::Sqrt((float)InstanceCount));
const float HalfGrid = (Grid - 1) * Spacing * 0.5f;
if (UBoxComponent* BoundsRoot = Cast<UBoxComponent>(RootComponent))
{
BoundsRoot->SetBoxExtent(FVector(HalfGrid + Spacing * 0.5f, HalfGrid + Spacing * 0.5f, Spacing), true);
}
CachedTransforms.SetNumUninitialized(InstanceCount);
int32 Index = 0;
for (int32 x = 0; x < Grid && Index < InstanceCount; ++x)
{
for (int32 y = 0; y < Grid && Index < InstanceCount; ++y)
{
FVector Pos(x * Spacing - HalfGrid,
y * Spacing - HalfGrid,
0.f);
FTransform T;
T.SetLocation(Pos);
T.SetScale3D(FVector(1));
CachedTransforms[Index] = T;
ISM->AddInstance(T);
Index++;
}
}
}
再次使用 Unreal Insights 分析后发现,当前主要耗时集中在:UInstancedStaticMeshComponent::BatchUpdateInstancesTransforms与DeferredRenderUpdates_GameThread这两个函数。
它们主要对应:
- 大量 Transform 数据的批量更新
- 向 GPU 上传实例数据
也就是大量实例的同步成本。
将实例数量提升到 100K 后,仍然可以稳定在 60FPS 左右运行,基本符合预期。
![]()
结论
可以看到,使用 InstancedStaticMesh 实现大量物体实例化渲染并进行实时更新时,性能压力主要集中在 CPU(Game Thread) 。
主要经验总结如下:
- 蓝图执行效率明显低于 C++,高频、大规模更新逻辑建议使用 C++ 实现
- 关闭不必要的功能(如 Collision)可以显著减少开销
- 如果包围盒变化不大,可以避免默认 Bounds 计算,使用自定义或固定方案
- 当实例数量达到较大规模时,Transform 批量更新与 GPU 数据上传是不可避免的主要成本
总体来说,在合理优化后,10 万级实例的实时更新依然可以达到较好的运行表现。
需要说明的是,本文的主要目的是探索 InstancedStaticMesh 组件在“海量实例 + 高频动态更新”场景下的性能边界,属于一次偏实验性质的性能测试与优化记录。
在真实项目中,如果确实存在大规模(例如数十万甚至更多)物体的持续更新需求,更推荐优先考虑:
- GPU Driven Rendering(GPU Instance / Compute / Niagara / 自定义 Instance Buffer 等方案)
- 或基于 ECS / 数据驱动架构的批处理更新方式
这类方案能够将大量计算从 CPU(Game Thread)转移到 GPU 或更高效的数据管线中,整体扩展性会更好。
因此,本文方案更适用于:
- 中等规模实例(几万~十万级)
- 或需要快速工程落地的 ISM 场景优化
而不是极限规模场景下的最终解决方案。