渲染顺序与带宽预算¶
帧率下降但不一定是 Draw Call¶
之前遇到过一个情况:场景里的 Draw Call 不算多,但一开屏幕后处理,帧率就明显下降。后来用 Frame Debugger 看,真正的问题是多张中间 Render Texture 在反复读写,带宽先撑不住了。
所以看渲染性能时,不能只盯着 Draw Call。渲染阶段、分辨率和像素格式都要一起看。
一帧里 CPU 做了什么¶
渲染管线在 CPU 侧通常可以分成四步:
- 收集相机、光源、Renderer 和后处理参数。
- 做视锥剔除、层过滤、遮挡剔除和 LOD 选择。
- 按队列、材质和距离排序。
- 设置 Render Target,提交绘制命令。
很多所谓的“合并 Draw Call”,其实发生在第 2、3 步。如果可见物体本来就很多,单靠合批不会解决问题。
URP 的常见顺序¶
一个普通的 URP 相机会经历这些阶段:
每个阶段都可能创建或读写 Render Texture。比如深度图、法线图、Bloom 的降采样链、TAA 的历史颜色,都要占显存和带宽。
自定义 Pass 的价值不只是“加一个全屏效果”,还要确认它放在哪个阶段、需要读什么、会写什么。放在 Opaque 前面还是后面,成本可能完全不同。
一张 Render Texture 有多贵¶
先按最简单的公式估算:
以 1920×1080、RGBA16F 为例,每像素 8 字节:
如果一个 Pass 读一次、写一次,大约是 31.6 MiB。按 60 FPS 算,每秒就是 1.85 GiB 左右的读写量。
如果开启 4× MSAA,这张 RT 的存储会变成约 63.2 MiB,读写带宽继续翻倍。实际 GPU 可能有压缩、Tile Memory 等优化,但量级不会差太多。
所以做后处理时,先看能不能降分辨率。Bloom、景深、屏幕空间反射这类效果,半分辨率往往已经够用。
透明物体和 Overdraw¶
透明物体通常不写深度,后面的像素仍然要执行片元着色。如果五层特效叠在同一个屏幕上,片元成本就接近五倍。
检查 Overdraw 时,可以临时把不透明物体关掉,只留特效,观察帧时间变化。也可以直接用 RenderDoc 抓一帧,看每个像素被着色了多少次。
减少 Overdraw 的常见办法:
- 缩小粒子面片,不要用一整张大图做局部效果。
- 能裁掉的部分在 Shader 里尽早
clip或丢弃。 - 透明排序保持稳定,避免无意义的大面积叠加。
- 低画质档位直接关掉不重要的后期效果。
Render Graph 管什么¶
Unity 6 的 URP 使用 Render Graph 组织 Pass。每个 Pass 声明自己读写哪些纹理,Render Graph 负责资源生命周期、依赖顺序和部分临时 RT 复用。1
一个 Pass 的基本结构大致如下:
using (var builder = renderGraph.AddRasterRenderPass<PassData>(
"Copy Color", out var passData))
{
passData.source = resources.activeColorTexture;
builder.UseTexture(passData.source);
builder.SetRenderAttachment(destination, 0);
builder.SetRenderFunc(static (PassData data, RasterGraphContext ctx) =>
{
// 在这里提交绘制或 Blit
});
}
这里省略了 PassData、destination 的创建和具体绘制命令。不同 URP 版本的 API 有差异,实际写法以项目锁定版本的文档为准。
Render Graph 能减少手动管理 RT 的错误,但不能替你做取舍。比如一个效果该不该半分辨率、该不该每帧执行,仍然要自己决定。
排查顺序¶
- 用 Frame Debugger 看 Pass 顺序和每个 Pass 的绘制对象。
- 用 GPU Profiler 看时间花在 ALU、纹理采样、带宽还是同步上。
- 检查中间 RT 的分辨率、格式和数量。
- 检查透明物体的覆盖面积和排序。
- 最后再看 Shader 指令数和变体数量。
一个小习惯:改动渲染效果时,同时记录帧时间和显存占用。只看截图很容易忽略成本,等低端机测试时再回头改,通常要重新调整整个效果链。