第一章:渲染管线与Draw Call
大家好,我是你们的老朋友。今天咱们聊聊渲染管线,还有那个让无数开发者头疼的Draw Call。
说实话,我刚入行那会儿,对渲染管线也是一知半解。直到有一次,我负责的一个AR项目在低端机上卡成了PPT,才逼着我彻底搞懂了这些东西。嗯,今天就把这些经验分享给你。
1.1 Unity渲染管线简介
渲染管线,说白了就是CPU和GPU怎么配合着把画面画出来的流程。我习惯把它分成三个阶段:
- 应用阶段(CPU干活):准备数据、做碰撞检测、决定哪些物体要画
- 几何阶段(GPU干活):顶点处理、裁剪、投影到屏幕
- 光栅化阶段(GPU干活):把三角形变成像素,上色
你想想看,CPU和GPU就像两个工人。CPU是项目经理,负责安排任务;GPU是画师,负责执行。如果项目经理安排任务太慢,画师就得等着——这就是性能瓶颈的根源。
核心观点:移动端性能优化的关键,就是让CPU少干活,让GPU多干活。
1.2 Draw Call的产生与影响
什么是Draw Call?每次CPU告诉GPU「嘿,把这个模型画出来」,就是一次Draw Call。
我举个例子。假设场景里有100个不同的茶壶,每个茶壶材质不同。CPU就得喊100次「画这个茶壶」。每次喊话都有开销——准备顶点数据、设置渲染状态、提交命令。这些开销累加起来,帧率就崩了。
为什么会这样?因为CPU和GPU是异步工作的。CPU提交命令的速度如果跟不上GPU的渲染速度,GPU就会空转等待。移动端CPU本来就弱,这个问题更明显。
| 设备类型 | 建议最大Draw Call数 | 备注 |
|---|---|---|
| 高端手机(骁龙8系) | 200-300 | 可接受,但需注意其他开销 |
| 中端手机(骁龙7系) | 100-150 | 需要严格控制 |
| 低端手机(骁龙4系) | 50-80 | 必须极限优化 |
避坑指南:我曾经在一个项目中,场景里放了300多个小道具,每个道具单独一个材质。结果在测试机上帧率直接掉到15帧。后来一查,Draw Call飙到了400多。嗯,这就是典型的「每个物体一个材质」的坑。
1.3 Batching技术
Batching就是合批,把多个小Draw Call合并成一个大Draw Call。Unity提供了两种方式:Static Batching和Dynamic Batching。
1.3.1 Static Batching
Static Batching适合那些不动的物体。比如地面、墙壁、树木这些静态场景元素。
原理很简单:Unity在构建时,把多个静态物体合并成一个大的网格。运行时只需要一次Draw Call就能画完。
使用条件:
- 物体必须标记为
Static - 共享同一个材质
- 顶点数不超过65535(单个合并网格的限制)
// 在代码中标记物体为静态
gameObject.isStatic = true;
// 或者直接在Inspector中勾选Static复选框
个人经验:我习惯把场景中所有不动的物体都打上Static标记。但要注意,Static Batching会增加内存占用,因为合并后的网格会常驻内存。如果你的场景特别大,建议分区域做Static Batching。
1.3.2 Dynamic Batching
Dynamic Batching适合运动的物体。Unity在运行时自动合并满足条件的物体。
条件比较苛刻:
- 每个物体顶点数不超过900(移动端建议更少)
- 共享同一个材质
- 不能有镜像变换(比如缩放为负数)
- 不能使用Lightmap
- 不能使用多Pass的Shader
说实话,Dynamic Batching在移动端效果有限。因为顶点数限制太严格了。一个稍微复杂点的模型就超过900顶点。
我的建议:移动端尽量别依赖Dynamic Batching。它带来的CPU开销有时候比省下的Draw Call还多。我更喜欢用GPU Instancing或者自己写合批逻辑。
1.4 合批失败的常见原因
合批失败,说白了就是Unity没法把多个物体合并到一起。我总结了几种最常见的情况:
- 材质不同:哪怕只是贴图不同,也会导致合批失败。这是最常见的原因。
- 顶点数超标:Dynamic Batching要求单个物体顶点数不超过900。Static Batching要求合并后总顶点数不超过65535。
- 使用了不同的Shader变体:比如一个物体用了Standard Shader,另一个用了Unlit Shader,肯定合不了。
- 物体有镜像变换:缩放为负数会导致法线方向错误,Unity会禁止合批。
- 使用了多Pass的Shader:比如一些特效Shader需要多个Pass渲染,每个Pass都会产生独立的Draw Call。
| 失败原因 | 解决方案 | 优先级 |
|---|---|---|
| 材质不同 | 使用材质池,共享材质 | 高 |
| 顶点数超标 | 拆分模型,或使用GPU Instancing | 中 |
| Shader变体不同 | 统一Shader,使用材质Property Block | 高 |
| 镜像变换 | 避免负缩放,用旋转代替 | 低 |
| 多Pass Shader | 改用单Pass Shader | 中 |
我曾经踩过的坑:有一次,我为了省事,把所有物体的材质都设成了同一个材质球。结果发现有些物体颜色不一样——我用的是MaterialPropertyBlock来修改颜色。但问题来了,PropertyBlock虽然能改颜色,但Unity会认为这些物体「材质不同」,合批就失败了。后来我改用Texture来存储颜色信息,才解决了这个问题。
小结
这一章我们聊了渲染管线的基本流程,Draw Call是怎么产生的,以及两种Batching技术。说白了,合批的核心就是让CPU少喊话,让GPU多干活。
下一章我会讲GPU Instancing和SRP Batcher,这两个是移动端性能优化的利器。到时候咱们再细聊。
记住一句话:移动端优化,从控制Draw Call开始。
如果你在项目中遇到合批问题,可以用Unity的Frame Debugger查看每个Draw Call的合批状态。这个工具我几乎天天用,强烈推荐。