GPU 剖析(11040 篇)解决的是"单次绘制为什么这么贵",这篇解决另一个正交问题:绘制次数为什么这么多。stat scenerendering 里 Draw 数几千上万、RHIThread 和 GPU 的 submission 排队时,砍数量就是主战场。UE 给的三条路线机制完全不同,选错的代价是"批降下来了,帧率没动"。
先建模:draw call 的代价在哪
一次 draw call 的成本可以拆成两块:
$$T_{frame} \approx N_{draw} \times \big( c_{cmd} + c_{state} \big) + T_{gpu}$$
$c_{cmd}$ 是命令编码、提交队列的固定开销(每 draw 都有,和画什么无关),$c_{state}$ 是状态切换开销——换成什么 shader、什么顶点格式、什么纹理,增量各不相同。经验排序是:换 shader/pipeline $\gg$ 换顶点缓冲 > 换纹理 > 换光栅化状态。所以"两个 mesh 用同一材质"和"两个 mesh 各用各的材质",合并的收益差一个量级——前者近乎免费,后者要么合不了要么得先合材质。
另一个关键事实:现代 GPU 画几千个简单三角形本身很快,瓶颈通常在 CPU 侧提交(RenderThread 编码 + RHI 提交 + 驱动)。所以"draw call 太多"的病根大多是 CPU,优化手段也大多在 CPU 侧做文章。
路线一:离线合并(Mesh Merge / Merge Actors)
思路最朴素:把同材质的静态小物件在离线(编辑器)时焊成一个网格,运行时一次绘制。UE 的工具是 Merge Actors(Actor Merging),产出可以是单个 static mesh,也可以是带有 LOD 的合并资产。
能合的前提和代价:
- 材质必须一致,或愿意合材质/打图集。两个材质各 30 个指令,合并后一个 50 指令的材质,总像素开销可能反而涨了——合并省的是 CPU,赔的是 overdraw 和着色复杂度,两边都要算;
- 合并后失去逐个剔除。十个物件各自可能在视锥外,合一之后只要有一个在视野内就得整包提交,顶点量翻倍。这也是合并只适合"小物件密集且基本同屏出现"场景的原因;
- 失去独立移动/销毁。Runtime 里要单独动其中一个,这条路线直接出局。
实测经验:室内布景、道具堆这类场景,合并常常能把 Draw 数砍一半以上,是性价比最高的首刀。
路线二:GPU 实例化(Instancing)
实例化是另一种思路:网格和材质不变,只是"画 N 份",每份只带一个 transform(和可选的 custom data)。GPU 一次绘制完成,CPU 侧成本从 $N \times c_{cmd}$ 降到 $1 \times c_{cmd} + N \times c_{instance}$,$c_{instance} \ll c_{cmd}$。
UE 里的落地:
- UInstancedStaticMeshComponent:手动放的实例网格,Foliage 系统底层就是它;
- 引擎自动实例化:MeshDrawCommands 会把场景中同 mesh + 同材质的绘制请求自动合并成 instanced draw,这一步不用你做任何事,所以"摆了一千个同材质石柱"本身不一定是问题;
- Per-instance 定制:换颜色、换材质参数走 Custom Data;换材质就破功——材质不同就是不同的 pipeline,自动实例化也救不了。
所以实例化的心法是:复用网格、复用材质,用参数表达差异。植被、栏杆、砖块这类"同模异色"内容天生适合。
路线三:HLOD(层级化空间聚类)
合并和实例化管的是"中近景怎么少画",HLOD 管的是"远景为什么还有 draw call"。远处一片建筑群,单个物件看都看不见了,但它们还挂在场景里、各自占着一次提交——一千个小物件在远方就是一千次空转的 draw call。
HLOD 的做法是按空间把物件递归聚类成树:叶子是单个物件,往上每层的簇生成一个合并的代理网格(Proxy Mesh,还自带简化材质)。相机拉远,整簇切换成一个低模代理,一次提交画完。距离阈值在 Build HLOD 时配置,引擎自动处理切换。
代价要认账:
- 内存:代理网格是额外资产,建筑密集的图 HLOD 数据可能占掉几百 MB;
- popping:切换瞬间的跳变,要么靠距离带调宽一点,要么开 dither 过渡;
- 构建时间:大场景 Build HLOD 动辄几十分钟到小时级,流程要排进日常迭代;
- 动态内容不参与:会动的物件不进 HLOD 簇,这是设计使然,别强塞。
骨骼网格的特例
角色身上几十个部件(护甲、挂件)各自是 skeletal mesh,draw call 数随部件数线性涨。两条路:Skeletal Mesh Merge 把部件焊成一个带分段材质的骨骼网格(材质数尽量合并),或者 LeaderPose/MasterPose 让挂件共享主网格的骨骼 pose——前者解决 draw call,后者解决动画开销,可以叠加用。
怎么选
给一个可执行的判断流程:先用 stat scenerendering 确认瓶颈是 Draw 数而不是 GPU 耗时(降批后帧率不动就说明瓶颈在别处);静态布景优先 Merge Actors;大量重复物件用实例化并守住"同材质"红线;中远景 draw call 堆积上 HLOD;角色部件走 Skeletal Merge。四条路不互斥,一个场景通常三管齐下,顺序按"CPU 提交耗时占帧时间比例"排。
收尾检查
降批不是目的,是手段。每一刀之后回到 stat unit 和 stat scenerendering 对账:Draw 数降了、CPU 帧时间降了才算数;只降 Draw 不降帧,说明瓶颈早就转移到 GPU 或同步点上,继续在这里堆优化就是浪费。