技术博客

Nanite 光栅化与 VisBuffer:软件光栅写可见性缓存

第 33 / 100 篇 · 2026-08-07 · ue5 · 渲染Nanite源码分析

上一篇(Cluster 剔除)讲完几何怎么被筛进视野,这一篇讲筛进来之后怎么变成屏幕上的像素。Nanite 的答案是 VisBuffer(Visibility Buffer):先记录"每个像素是哪个三角形的",再按材质批量着色。源码:Runtime/Engine/Private/Nanite/ 下的 NaniteDrawCommands.cpp、以及 Shaders/Private/NaniteRasterizer.usf(UE5.3)。

为什么不用传统管线

传统 forward/deferred 的像素着色直接挂在光栅化后面,一个像素被 5 个三角形盖着就最多跑 5 次像素着色(深度早退能省一部分,但顶点着色和光栅化开销跑不掉)。Nanite 面对的场景是百万级三角形、海量小簇,问题被放大到不能接受。解法是拆成两步:

  1. 光栅化只写可见性:每个像素记一个 ID——"这个像素最终被簇 $c$ 的三角形 $t$ 覆盖";
  2. 着色按 ID 反查:把 ID 相同的像素聚成 batch,一次性取回三角形数据(位置、UV、法线来自顶点数据 + 簇表查表),跑材质着色。

着色次数从"三角形数 × 过绘制"降为"实际可见的像素数",过绘制(overdraw)在像素着色阶段的代价被消除——第一层三角形白画只是写个 64 位整数,比跑材质便宜两个量级。这就是"可见性缓存"这个名字的含义:它是可见性判定结果的缓存,不是颜色缓存。

软硬两条光栅路径

VisBuffer 阶段 Nanite 用两种光栅器,按簇的屏幕面积分流(NaniteRasterizer.usf 及 NaniteDrawCommands.cpp 中的派发逻辑):

  • 硬件光栅(RasterizeVS/PS):走 GPU 固定管线。大三角形效率高,小三角形被面积率拖累——每个 3 像素的小三角形都要走一遍完整的顶点处理 + 三角形组装。
  • 软件光栅(Compute shader RasterizeCS):簇内三角形在 compute 里逐三角形扫描。每个线程处理一个 2×2 像素 quad(quads 对齐采样栅格),对三角形包围盒内的 quad 做覆盖率测试和深度比较。小三角形(阈值以下,跟 r.Nanite 的屏幕误差阈值同源)走这条路,绕开固定管线的逐三角形开销。

分流的依据是簇投影到屏幕后的面积。设簇内平均三角形数 $n_t$,每个三角形平均覆盖 $a$ 像素:固定管线单三角形有近似常数的组装开销 $c_0$,软件光栅单像素有常数开销 $c_1$,单簇硬件成本 $\approx n_t \cdot c_0$,软件成本 $\approx n_t \cdot a \cdot c_1$。交点在 $a^* = c_0 / c_1$ 附近——屏幕占比小于 $a^*$ 的三角形软件光栅划算,大于则硬件划算。UE 的实现就是这个常数比较(具体阈值随版本调,思路不变)。远处的几何几乎全落在软件侧,这正是 Nanite"远景三角形多了不卡"的机制之一。

VisBuffer 里存了什么

64 位像素:簇索引(高 32 位中的高位段) | 三角形索引 | 深度。深度在同一阶段写入深度缓冲,VisBuffer 和 depth 成对使用。后续 pass 拿像素 ID 反查:簇表 → 三角形顶点索引 → 顶点缓冲,取插值属性。材质需要的插值在着色 pass 里由像素位置 + 重心坐标现算(Interpolate 系列函数),不占 VisBuffer 空间。

这带来一个 trade-off:VisBuffer 方案下材质属性获取是"随机访问顶点数据",缓存命中率取决于像素聚类质量——所以下一个 pass 必须按材质分 batch。

材质分类 pass:把像素排序成 draw call

Nanite 材质是逐三角形划分的(一个簇内不同材质区段 → 不同的"材质段")。着色前的 ClassifyMaterials(NaniteShadeBinning,按材质 binning)把 VisBuffer 按材质段统计像素、生成间接 dispatch 参数,同材质的像素合成一次 compute dispatch。这一步是连接 VisBuffer 和材质系统的桥:几何侧(簇/三角形 ID)和着色侧(材质 shader)在这里握手。

解耦的红利在加新材质时最明显:不用动光栅化侧任何代码,新材质只是多一个 bin。代价是 binning pass 自身有开销(计数 + 前缀和 + 间接参数),材质种类爆炸(几十个材质段挤一个簇)会让 dispatch 碎化——美术侧控制簇内材质数量有真实性能含义。

与现代 API 的关系

VisBuffer 并不是 Nanite 专利(CryEngine 的 Culling Revamp、DOOM 2016 的 ID 缓冲都是前身),但 Nanite 把它推到生产可用的规模:依赖前面剔除管线把簇规模控制到光栅器吞吐内,依赖 RDG 把 VisBuffer 之后的一串 pass(分类、着色、GBuffer 组装)组织成数据流。理解这条链的正确顺序是:剔除决定"画哪些簇",VisBuffer 决定"哪些像素获胜",分类决定"按什么顺序着色"——三者都服务于同一个目标:让像素着色次数逼近理论下限(可见像素数),而不是三角形提交数的倍数。

VisBuffer 流水线:可见性记录与着色解耦 簇剔除后 屏幕面积已知 分流 大簇硬件 / 小簇软件 VisBuffer + 深度 每像素 64 位簇/三角形 ID 材质分类 binning 按材质段聚成 dispatch 着色阶段:ID 反查 簇表 → 顶点索引 → 顶点缓冲;重心坐标现算插值;同 bin 像素一次 dispatch 跑材质 过绘制代价被压到"写 64 位整数",材质着色只跑在最终可见像素上 硬件 vs 软件的交点 硬件单簇成本 ≈ n_t·c_0(组装) 软件单簇成本 ≈ n_t·a·c_1(扫描) a < c_0/c_1 走软件,反之走硬件 c_0:固定管线逐三角形常数开销 c_1:compute 单像素覆盖率测试开销