资讯动态

UE4性能优化:从Spawn到HISM,高效渲染大规模实例物体

发布时间:2026/9/16 2:46:48 来源:尧图企业网站定制
做 UE4 项目的朋友大概率都遇到过这样的场景要往关卡里撒几百个碎石、铺一片草地、放一地的金币或者道具。很多人第一反应是 Spawn也就是循环 SpawnActor把同一个 Static Mesh 的 Actor 复制出来。结果一跑起来帧率从 60 直接掉到 20 多打开 Profile 一看满屏都是同一个 Mesh 的 Draw CallCPU 侧的 Actor 数量也涨得吓人。其实这正是 Hierarchical Instanced Static MeshHISM组件该出场的时候。HISM 是 UE4 内置的一种实例化渲染组件官方用它做植被系统也就是 Foliage 的底层实现。它的核心思路是用一个组件、一次 Draw Call 搞定成千上万个相同 Mesh 的渲染同时通过层级空间结构加速裁剪和 LOD 切换。这篇文章我会从 Spawn 为什么慢讲起然后拆解 HISM 的原理再给出一套从 Spawn 迁移到 HISM 的完整实操流程最后把我踩过的坑和排查经验都列出来。适合正在做开放世界、大场景布置、大量拾取物或者任何需要批量摆放物体的开发者参考。1. 先搞清楚Spawn 为什么慢1.1 Actor 不是“一个模型”那么简单很多人想当然地认为SpawnActor 出来一个带 Static Mesh 的 Actor跟 HISM 里添加一个实例差不多。但实际差别非常大。每 SpawnActor 一次你得到的是一个完整的 AActor 对象它身上至少挂了一个 USceneComponent 和一个 UStaticMeshComponent还可能带碰撞、网络复制、事件绑定等默认开销。哪怕你关闭了 Tick这个 Actor 依然存在于引擎的级别里要参与场景更新、GC 引用追踪、序列化、编辑器标记等一系列工作。从渲染管线角度看一个 Static Mesh Component 正常提交时至少要产生一次 Draw Call如果这个 Mesh 内部有多个 Section 或者多个材质元素Draw Call 会成倍增加。也就是说你用 Spawn 复制 1000 个碎石渲染线程收到的就是 1000 次 Draw CallCPU 侧则要遍历这 1000 个组件各自的裁剪、遮挡剔除、距离计算。引擎再优化也顶不住这种“一个实例一套完整组件”的架构。1.2 一场“拾取物”压垮帧率的实测我之前做过一个原型在一个 500×500 的测试关卡里用 SpawnActor 铺了 800 个金币模型每个金币是一个 300 面左右的低模。场景里没有任何敌人、没有任何逻辑只是让角色在中间跑。结果怎样呢帧率从空场景的 144 帧降到了 45 帧左右Stat GPU 显示 Draw Call 大概从几十涨到了接近 900。大部分开销并不是像素填充而是 CPU 侧的组件提交和状态切换。我又试过把数量加到 3000帧率直接掉到 18 帧左右而且内存上涨非常明显因为每个 Actor 都有独立的对象头、组件列表、属性表。那一瞬间我就明白用 Spawn 做“数量上规模的同构物体”是战略错误。它不是某个参数没调好而是架构上就不适合。1.3 什么时候 Spawn 仍可接受当然我不是让你把所有 Spawn 都废弃。反过来讲如果物体需要独立逻辑、独立动画、独立物理模拟、独立交互事件比如 NPC、怪物、可以被拾取并播放动画的钥匙那就不适合用 HISM。每个实例在 HISM 里本质上是“一堆变换数据”没有单独的事件或行为你很难对某一个实例附加复杂逻辑。我的经验是当同屏同类物体数量超过一百而且它们的行为趋同、不需要各自 Tick 时就应该条件反射地想到实例化渲染。如果数量只有几十个用 Spawn 完全没问题别为了炫技强行上 HISM反而增加不必要的复杂度。2. HISM 到底快在哪2.1 从一个 Static Mesh 渲染百万个实例HISM 全称是 Hierarchical Instanced Static Mesh核心是两个词Instanced 和 Hierarchical。先说 Instanced。它本质上利用了 GPU Instancing 技术同一个 Static Mesh 的顶点数据、索引缓冲、材质参数只在显存里存一份渲染时把这批实例的变换矩阵打包成一个 Instance BufferGPU 在一个 Draw Call 内根据每个实例的矩阵重复绘制同一个 Mesh。打个生活化的比方普通 Spawn 相当于你复印了 1000 张图纸叫 1000 次快递分别送出去编辑器里每个快递都要单独分拣、单独立单。而 Instancing 就像一张订单写清楚“这 1000 个东西都送到同一个地址”快递车一次拉过去收货点按地址逐个分发。所以 Draw Call 从 N 次直接降到了大约 1 次这个收益在几百上千个实例时极其明显。2.2 层级结构是“更聪明”的部分如果只是简单 Instancing那叫 ISMInstanced Static Mesh。HISM 在此基础上多了一个“层级”结构引擎会把场景中的实例按照空间位置聚成一簇一簇的 cluster再对 cluster 构建一棵树。剔除的时候如果整棵子树在视锥外或遮挡关系不成立这棵子树下的所有实例就可以一次性跳过不用逐个判断。这个优化最典型的收益体现在大面积草地的场景。一簇草可能在屏幕里只占几个像素HISM 直接整簇剔除如果按照 ISM 逐实例遍历哪怕实例本身很便宜CPU 遍历和提交的总量也会撑爆。HISM 还有一套基于簇的 LOD 切换机制远处簇直接切低模甚至成簇消失比逐个实例计算 LOD 高效得多。这也是为什么 UE4 的植被系统最终选择 HISM 作为底层。2.3 HISM vs ISM vs 常规 Actor 的取舍表我自己做选型时习惯画一张表把需求往里套。下面这个表基本覆盖了常见判断维度对比维度SpawnActor StaticMeshISMHISMCPU 裁剪开销高逐个组件遍历中逐实例遍历低按簇递归裁剪Draw CallN 个实例 ≈ N 次每个 Mesh/材质 1 次每个 Mesh/材质 1 次内存开销高每个 Actor 一套对象中只有实例数据中上实例数据 树结构动态增删实例灵活但开销大支持但更新效率一般支持批量操作最优LOD 支持每个组件独立 LOD整体只能用固定 LOD支持按簇混合 LOD有无碰撞每个组件自带灵活支持实例碰撞支持实例碰撞但代价较高适合场景少量交互物、NPC静态批量物体超大数量、植被、开放世界看到这个表你应该明白了HISM 不是在所有维度上都是最优的。它最大的代价是内部那棵树的维护。如果你频繁微调单个实例的 Transform触发树重建的代价会很高。因此静态且数量巨大的物体是它的主战场动态批量更新的场景需要谨慎设计。3. 实操从 Spawn 改成 HISM 的完整流程3.1 创建 HISM 组件的两种姿势蓝图里最直接的方式是给 Actor 添加组件搜索 Hierarchical Instanced Static Mesh拖进来后把 Static Mesh 设置成目标模型。然后在 BeginPlay 里调用 Add Instance传入 FTransform 即可。C 项目里更推荐在构造函数里创建和配置组件。下面是一个最小可用的 Actor 类示例// MyHISMTestActor.h #pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include MyHISMTestActor.generated.h UCLASS() class MYPROJECT_API AMyHISMTestActor : public AActor { GENERATED_BODY() public: AMyHISMTestActor(); UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category HISM) class UHierarchicalInstancedStaticMeshComponent* HISMComponent; };// MyHISMTestActor.cpp #include MyHISMTestActor.h #include Components/HierarchicalInstancedStaticMeshComponent.h AMyHISMTestActor::AMyHISMTestActor() { PrimaryActorTick.bCanEverTick false; HISMComponent CreateDefaultSubobjectUHierarchicalInstancedStaticMeshComponent(TEXT(HISMComponent)); RootComponent HISMComponent; // 这里建议先不设置 StaticMesh留给蓝图或代码在运行时指定 // 但如果你确定模型资源也可以在构造函数里直接加载。 }PrimaryActorTick.bCanEverTick false顺手关掉 Tick因为 HISM 场景根本不需要这个 Actor 每一帧去刷逻辑。3.2 核心 API添加、删除、更新实例配置好组件后最常用的接口就是 AddInstance、AddInstances、RemoveInstance、UpdateInstanceTransform 和 ClearInstances。我直接给一段批量添加 10000 个实例的参考代码void AMyHISMTestActor::InitInstances() { if (!HISMComponent || !HISMComponent-GetStaticMesh()) { return; } TArrayFTransform Transforms; Transforms.Reserve(10000); FRandomStream Stream(12345); for (int32 Index 0; Index 10000; Index) { FVector Location( Stream.FRandRange(-5000.0f, 5000.0f), Stream.FRandRange(-5000.0f, 5000.0f), StartHeight ); FRotator Rotation(0.0f, Stream.FRandRange(0.0f, 360.0f), 0.0f); FVector Scale(Stream.FRandRange(0.8f, 1.2f)); Transforms.Add(FTransform(Rotation, Location, Scale)); } // 批量添加一次性交给引擎 HISMComponent-AddInstances(Transforms, false); // 添加完成后手动重建内部树确保后续裁剪、LOD 立即生效 HISMComponent-BuildTreeIfOutdated(); }这里有个细节很多人容易忽略AddInstances 的第二个参数是 bWorldSpace意思是传入的 Transform 是相对于世界空间还是相对于组件。如果你把 HISMComponent 挂在一个会整体移动的根组件下面千万想清楚用哪种模式或者统一在局部空间里更新。关于移除RemoveInstance 会移除指定索引的实例。要注意的是删除后剩下的实例索引会重新排列如果你的业务逻辑里保存了实例索引数组删除后必须做索引同步。我习惯的做法是“池化”不真正删除而是把不用的实例移动到很远的坐标并隐藏等需要的时候再重新设置 Transform。这样绕开了树重建的代价。3.3 关键参数与推荐配置HISM 的参数不少但真正影响性能的就那么几个。我在项目里常用的配置如下参数推荐值说明MinInstanceCount50低于这个数量时组件退化为普通 ISM 逻辑数值越小越早构建树但重建也更频繁bAutoRebuildTreeOnInstanceChangesfalse运行时大量增删时建议关闭手动批量操作后重建InstanceStartCullDistance / InstanceEndCullDistance按场景距离设置实例级裁剪距离建议从近到远分层bUseDynamicInstanceBuffer视情况开启如果材质使用 World Position Offset 或要频繁移动实例可以开启CollisionEnabledQueryOnly 或 NoCollision对纯展示对象建议 NoCollision否则实例数量大了很亏MinInstanceCount是我第一次用 HISM 时踩过的坑。默认值是 50也就是说实例数量少于 50 时组件可能不会真正构建层级树直接按 ISM 处理。当时我做了个测试加了 30 个实例发现性能跟普通 ISM 没差别一度怀疑 HISM 没用。后来把数量拉到几百个差别才体现出来。所以如果你的目标实例数很小用 HISM 反而没有意义。3.4 常见应用落地案例先说说我最常用的三个场景。第一个是草地。做法是准备两三种草的 Static Mesh建一个 HISM 组件然后按地面采样点随机旋转、缩放批量添加。草不需要碰撞直接 NoCollision开启双面材质配合 wind 的 World Position Offset效果能打。第二个是场景里的装饰性碎石、垃圾、瓶瓶罐罐。它们数量多但玩家不会交互也用 HISM。第三类是拾取物比如金币。严格来说这些物体需要交互但拾取行为可以这样设计先用 HISM 批量生成玩家触发拾取后把对应实例移到地图外并隐藏等刷新时间到了再移回来。这样比每拾取一个就 DestroyActor 一个要平滑得多。关于每个实例的随机颜色或随机参数可以用 PerInstanceCustomData 或者 CustomPrimitiveData把随机浮点数组传给材质。这个我后面章节会讲到但先说结论HISM 自带实例随机化变量的能力不需要每实例拆材质。4. 常见问题与坑4.1 运行时动态增删性能突然卡顿这是 HISM 最大的“隐藏坑”。很多人发现明明渲染节省了 Draw Call但运行时 AddInstances 一大批次帧率会突然卡一下。原因在于引擎需要重建内部的层级树特别是实例数很大时聚类计算和 LOD 分配会占用一定 CPU 时间。我的处理经验有三条。第一尽量在加载阶段或 Enter 关卡时一次性批量添加不要每帧零星添加。第二如果确实要运行时大量生成把 bAutoRebuildTreeOnInstanceChanges 临时设为 false等整批操作完成后再调用 BuildTreeIfOutdated。第三动态移除时优先用“位移隐藏”代替 RemoveInstance通过 SetVisibility 或把实例移到超远距离避免频繁改动树结构。4.2 碰撞与物理的坑HISM 的碰撞分为简单碰撞和复杂碰撞但不管哪种实例数量大之后碰撞查询开销都不低。纯展示的草、碎石直接把 CollisionEnabled 设为 NoCollision。如果必须碰撞我只给一部分实例开启或者用简化盒体而不是每个实例都用复杂碰撞体。还有一个常见误解HISM 循环里你写 Physics Simulation每个实例并不会自动获得刚体物理模拟能力。它只是渲染实例化物理世界不会因为你在 HISM 里塞了模型就为每个实例生成物理体。如果你确实需要物体可被推动、掉落那就别硬用 HISM老实回到 SpawnActor。4.3 材质、LOD 与渲染相关的坑材质方面很多刚上手的人会疑惑为什么 HISM 里的世界位置偏移不生效。这个跟 bUseDynamicInstanceBuffer 有关。如果你在材质里用了 World Position Offset建议开启这个选项否则部分平台会表现异常。另外任何逐实例动画如果不能完全放进材质就不适合 HISM。LOD 方面务必确认 Static Mesh 已设置 LOD并且在 HISM 组件上开启 LOD 相关选项。HISM 对 LOD 的处理是簇粒度的它会根据距离给不同簇分配不同 LOD 等级。如果 Mesh 只有一个 LOD那 HISM 的 LOD 优化就无从谈起。还有实例距离裁剪的 Start 和 End 之间建议给一个过渡带避免物体在远处突然全部消失或者像纸片一样闪没。5. 实际项目中的优化扩展5.1 与 Foliage 模式的关系UE4 编辑器内置的植被刷工具底层用的就是 HISM 组件。你手动刷出来的每一棵草本质上是给同一个 HISM 组件添加了一个实例。知道这层关系后很多操作就通透了你可以先在编辑器里用 Foliage 刷好布局运行时再把新物体实例添加进同一个组件也可以完全在代码里动态生成两者可以混用。官方之所以敢让你刷几万棵草就是因为它把整个草地收拢在若干 HISM 组件里。5.2 结合 Nanite 与移动端注意事项UE5 的 Nanite 发布后很多人问 HISM 是不是被淘汰了。实际上 Nanite 解决的是三角形提交和像素层面剔除的问题而 HISM 解决的是批量实例管理的问题。两者可以配合Nanite 网格也可以放进 HISM 组件使用。不过要注意Nanite 的几何体在低端硬件上有额外开销移动端还是要谨慎。移动端使用 HISM 时我建议把实例数控制在一万以内并且不要开太高的阴影。移动 GPU 的实例缓冲带宽有限实例数过大会把内存带宽打满Draw Call 虽然降下来了但性能依然上不去。如果项目有包体或显存预算要求先做真机测试再决定实例规模。5.3 性能验证方法Stat GPU / ProfileGPU不要只凭感觉判断优化效果。添加 HISM 前后打开控制台输入Stat GPU看 Draw Call 和渲染线程耗时输入ProfileGPU抓一帧看 HISM 对应的 Mesh 提交只占多少毫秒。我习惯再开Stat Scenerendering查看实例数、裁剪掉的实例比例。如果裁剪比例很低说明你的实例可能过于集中或者层级树没有真正生效。还有一个容易被忽略的验证点看 CPU 帧时间。HISM 的收益主要体现在两个层面一个是渲染线程的 Draw Call 下降另一个是游戏线程的组件遍历下降。只盯着 GPU 时间看可能会漏掉真实收益。用 Unreal Insights 或简单的stat unit都能看到各线程耗时占比。另外想说一个小技巧调试时可以在 HISM 组件上开启 Debug 绘制把每一簇的包围盒画出来。这样能直观看到层级树是否合理比如是否出现了异常大的簇导致裁剪失效。这个在开发阶段非常管用能帮你快速定位到底是资源问题还是配置问题。我在实际项目里最深的体会是HISM 并不是一个“性能黑魔法”它的所有优势都建立在“大量同构、静态、无需独立逻辑”这个前提上。用对地方它是一个组件撑起整个场景用错地方你会在动态更新和碰撞上付出更高成本。所以遇到批量物体需求时先冷静做一次选型判断把物体分成“要逻辑的”和“纯摆件的”两类然后再决定哪些走 Spawn、哪些走 HISM。最后分享一个我常用的落地思路任何新场景默认把地面石子、墙面装饰、远处建筑群、大量可交互的非角色道具全部规划到 HISM 实例池里只有需要独立动画和事件的对象才留下 SpawnActor 路径。然后把批量的添加、更新、隐藏都封装成一个简单的实例池管理类业务层只跟索引打交道。这样既拿到了性能又不会把代码写成一团乱麻。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价