3、光栅化阶段:像素填充率的秘密,Early-Z与Hi-Z的实战应用

光栅化阶段,说白了就是把顶点数据变成屏幕上的像素点。很多开发者觉得这步是GPU自动完成的,没什么好调的。嗯,我以前也这么想,直到被一个项目的帧率问题折磨了两周。

那次项目其实很简单,一个3D场景,模型面数不算多,但帧率死活上不去。我查了半天,发现GPU的像素填充率已经爆了。你想想看,一个像素可能要经过几十次甚至上百次的Overdraw才能最终显示出来,这性能能好吗?

所以今天我们就来聊聊像素填充率的秘密,以及Early-Z和Hi-Z这两个救命稻草怎么用。

3.1 像素填充率:GPU的隐形天花板

像素填充率,就是GPU每秒能处理多少个像素。单位是GPixel/s。公式很简单:

像素填充率 = 核心频率 × 像素管线数量

举个例子,Adreno 650的频率是587MHz,有4个像素管线,那它的像素填充率就是:

587 MHz × 4 = 2.348 GPixel/s

听起来挺高的对吧?但实际项目中,一个1080p的屏幕有207万像素。如果每帧有10层Overdraw,那每帧就要处理2070万个像素。60帧的话,每秒就是12.4亿个像素。嗯,已经接近极限了。

关键点:像素填充率是GPU的硬性指标,一旦超过,帧率就会断崖式下降。不是慢慢掉帧,是直接卡成PPT。

我在项目中遇到过最夸张的情况,一个场景的Overdraw达到了30层。你想想看,一个像素被画了30次,前29次都是白费的。那款手机GPU的像素填充率只有1.8 GPixel/s,1080p@60fps根本跑不动。最后只能砍特效,降低分辨率。

3.2 Early-Z:提前淘汰,减少无效绘制

Early-Z,全称是Early Depth Test。它的原理很简单:在像素着色器执行之前,先做一次深度测试。如果这个像素被挡住了,那就不用执行像素着色器了。

没有Early-Z的时候,流程是这样的:

  1. 顶点着色器处理顶点
  2. 光栅化生成像素
  3. 像素着色器计算颜色
  4. 深度测试,被挡住就丢弃

有Early-Z的时候:

  1. 顶点着色器处理顶点
  2. 光栅化生成像素
  3. 先做深度测试,被挡住就直接丢弃
  4. 只有通过测试的像素才执行像素着色器

你看,像素着色器是GPU里最耗资源的阶段。Early-Z能提前砍掉大量无效像素,节省的带宽和计算量非常可观。

实战技巧:Early-Z生效的前提是,你的绘制顺序是从近到远。如果从远到近画,那Early-Z基本没用,因为远处的像素先画,近处的像素后画,深度测试永远过不了。

我曾经在一个项目中,把绘制顺序从远到近改成了从近到远,Overdraw从15层降到了3层。帧率直接翻倍。就这么简单的一个改动,效果立竿见影。

3.3 Hi-Z:更聪明的深度剔除

Hi-Z,全称是Hierarchical Depth Buffer。它比Early-Z更聪明。Early-Z是对每个像素做深度测试,而Hi-Z是对一个像素块(通常是8×8或16×16)做测试。

Hi-Z的工作原理是这样的:

  • GPU维护一个深度缓冲区的层级结构
  • 每个层级存储一个像素块的最大深度和最小深度
  • 绘制新像素时,先检查它所在的像素块
  • 如果整个块都被挡住了,那这个块里的所有像素都不用画了

这就像你查字典,先查目录,再查具体页码。Hi-Z就是那个目录,能快速定位哪些区域完全不可见。

特性 Early-Z Hi-Z
测试粒度 像素级别 像素块级别
性能开销 较低 更低
适用场景 一般场景 大块遮挡场景
硬件支持 几乎所有GPU 较新GPU

注意:Hi-Z不是万能的。如果场景中都是细碎的物体,比如树叶、栅栏,那Hi-Z的效果就不明显。因为每个像素块里可能既有可见像素又有不可见像素,Hi-Z没法做整块剔除。

3.4 实战:如何利用Early-Z和Hi-Z优化性能

光说不练假把式。我们来看看实际项目中怎么用。

3.4.1 绘制顺序优化

这是最基础也最有效的方法。我建议你这样做:

// 错误的绘制顺序:从远到近
for (int i = objects.size() - 1; i >= 0; i--) {
    DrawObject(objects[i]);
}

// 正确的绘制顺序:从近到远
for (int i = 0; i < objects.size(); i++) {
    DrawObject(objects[i]);
}

你可能会问,为什么不透明物体要从近到远?因为近处的物体会挡住远处的,Early-Z能提前淘汰远处的像素。透明物体则相反,要从远到近画,因为透明需要混合。

3.4.2 深度预填充

这是一个高级技巧。先画一个深度通道,把场景的深度信息写进去,然后再画颜色通道。这样Early-Z和Hi-Z就能更高效地工作。

// 第一步:只写深度,不写颜色
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
DrawSceneDepthOnly();

// 第二步:正常绘制颜色
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
DrawSceneColor();

我在一个大型开放世界项目中用过这个技巧。场景里有大量的建筑和地形,Overdraw非常严重。用了深度预填充后,像素着色器的执行次数减少了60%。代价是多了一次深度绘制,但深度绘制比颜色绘制轻量得多,整体性能还是提升了。

3.4.3 避免Alpha Test

Alpha Test会破坏Early-Z。因为Alpha Test是在像素着色器里判断透明度,如果透明度不够就丢弃像素。但这时候Early-Z已经做完了,像素着色器已经执行了。

避坑指南:我曾经在一个项目中大量使用Alpha Test来做树叶的裁剪。结果Early-Z完全失效,Overdraw飙升。后来改用Alpha Blend + 排序,性能好了很多。记住,Alpha Test是Early-Z的杀手。

3.5 总结

像素填充率是GPU的硬性指标,一旦超过就会卡顿。Early-Z和Hi-Z是减少无效像素绘制的利器。核心要点就三个:

  • 绘制顺序从近到远,让Early-Z发挥最大作用
  • 深度预填充,提前建立深度信息
  • 避免Alpha Test,别让像素着色器白干活

嗯,这些技巧看起来简单,但实际项目中能坚持做好的团队不多。你想想看,一个场景里几十万个物体,绘制顺序稍微乱一点,性能就崩了。所以,从项目一开始就要建立规范,把绘制顺序管理好。

下一章我们会聊聊像素着色器的优化,那才是真正的性能黑洞。