做渲染引擎这些年,我越来越觉得“光栅化”这个名词已经装不下我们正在做的事情了。过去一说光栅化,大家脑子里就是三角形进、像素出,GPU里一条固定的硬件管线,性能稳定、结果可控。但这两年,微多边形概念的回归,“软硬协同”四个字频繁出现在引擎架构和渲染管线的设计讨论里,光栅化开始变成一件需要CPU和GPU、硬件固定单元和软件可编程逻辑协商着来的事情。
这篇文章我想认真拆一下这套架构:什么是微多边形时代的光栅化,为什么传统的固定功能光栅化器不够用了,软硬协同到底协同的是什么,以及这套思路在实时渲染、前端渲染、云渲染这些场景里到底怎么落地。适合正在做渲染引擎、图形学相关开发,或者想理解现代引擎内部设计逻辑的朋友阅读。我会尽量把原理讲得直白,把工程细节和踩坑经验也一并交代清楚。
1. 从三角形网格到微多边形:渲染架构的范式迁移
1.1 三角形吞吐量:显卡的“隐性天花板”
先说一个很多人在做渲染优化时都会撞上的墙:现代GPU的三角形吞吐量看起来很夸张,官方参数动辄每秒几十亿甚至上百亿三角形,但实际游戏项目里,一个场景几百万三角形就能把帧率压得喘不过气。原因在于,光栅化本身只是整个几何流水线的一环。三角形要从显存被读取、做顶点变换、做裁剪、做背面剔除、做LOD选择,然后才轮到固定功能单元去扫描转换。三角形数量越大,前面的步骤消耗就越不可忽视。
我见过很多项目的优化过程,第一反应是减面,把模型的顶点数从百万级别压到十万级别,换来一些帧率提升,但画面细节也跟着崩了。继续往下做,开始切LOD、做视锥剔除,这些都是常规操作,能解决一部分问题,但并不是架构层面的答案。真正值得关注的是:当一个模型靠近摄像机时,我们想要的是像素级的细节,而不是一堆平面三角形在那转着逼近曲面。用静态网格去满足这个需求,就需要极高密度的三角形,而高密度三角形进入固定功能光栅化器时,开销并不按比例变得划算。
这个现象背后其实引出了微多边形时代最核心的动机:我们要的不是更多三角形,而是在需要更多细节的地方,出现更多适当大小的几何单元。
1.2 微多边形的起源:从RenderMan到游戏引擎
微多边形这个概念并不新鲜。早在RenderMan时代,Pixar就在离线渲染里用micropolygon来逼近曲面。做法是把曲面切分成一个一个小到像素级别的面片,每个面片小到可以被当成一个点来处理。这种做法的厉害之处在于,几何密度不是由建模师手动决定的,而是由表面的曲率、着色的需求、与摄像机的距离自动决定的。整个渲染过程近似于“按需生成几何”。
到了实时渲染领域,这个思想以几种不同形态延续了下来。最早的一波是DX11时代的硬件细分曲面(Tessellation),用曲面细分控制点和细分因子动态增加网格密度。再往后是Compute Shader驱动的视差映射、位移贴图,通过在像素着色器里做高度场采样模拟表面起伏。最近最典型的代表则是UE5的Nanite,它把场景里的网格切成一个个cluster,按屏幕空间误差决定要加载哪一级,再把级别最高的微小三角形直接塞给软件光栅化器处理。
这些方案看似各不相同,内核却高度一致:几何不再是预先烘焙好的静态三角网格,而是在渲染时动态生成的细粒度微几何单元。微多边形的本质不是“无限细分”,而是“按需出现细节”,在屏幕上占不到一个像素的面片不需要走传统的三角形光栅化流程,可以直接当成点来处理。
1.3 微多边形“自适应”的工程价值
从工程角度往回看,微多边形的价值比表面看上去要大得多。传统LOD最让人头疼的问题是切换弹跳。你靠近一棵树,引擎在两个LOD之间切换,要么忽然掉一块细节,要么树皮上冒出一堆不稳定的闪烁。美术同学调半天LOD距离和过渡范围,依然很难做到无缝。
微多边形的自适应方案天然规避了这个问题。因为细分是连续的,屏幕空间误差是平滑变化的,你可以根据距离、视角、甚至材质粗糙度来控制几何密度,让细节像水一样慢慢涨起来,而不是像台阶一样跳变。这也是为什么很多新的渲染架构开始重新审视“几何表示”这个老问题的原因。静态三角网格是一种很好理解、很好存储的表示,但它把“细节”锁死在了建模时。微多边形把细节还原到了渲染时,这是一次成本模式上的重大转移。
我在实际项目里折腾这类架构时最大的感受是:前期的几何处理逻辑复杂度会显著上升,但渲染质量的提升和内存占用的下降非常明显。只要你扛过了最初的架构阵痛期,后面整个渲染流程都会变得更“聪明”。
2. 光栅化管线的软硬分工:谁在做什么
2.1 固定功能光栅化器:又快又死板
经典的硬件光栅化,指的是GPU里那条固定的几何处理流程:顶点着色器处理完顶点,图元装配阶段把三角形组织好,固定功能的扫描转换单元把三角形覆盖到像素,像素着色器计算颜色,深度测试决定谁挡住谁。
这条管线的最大优点是确定性高、性能极其稳定。现代GPU的三角形设置单元用了非常成熟的分块扫描策略,对常规尺寸的三角形,吞吐量非常高。而且整个流程是高度并行的,硬件厂商花了几十年时间把扫描转换、深度缓冲区读写这些操作打磨到了极致。
但它的短板也很明显:只会写三角形、线、点这几类图元。当你需要的几何单元不是标准三角形时,硬件就无能为力了。比如微多边形的面片小到几个像素甚至不到一个像素,硬件上把它当三角形处理,要经历完整的三角形设置、扫描转换、插值流程,而实际上它完全可以简化为一个点的操作。硬件在这里不但没有加分,反而因为固定开销变成了负担。
还有一个现实问题是,固定功能光栅化器对“非常规”渲染需求的适配能力很弱。比如精确的遮挡剔除、自定义的像素写入规则、高精度的深度比较,这些东西要么需要开很多附加状态和扩展,要么干脆做不了。艺术家们喜欢在渲染器里玩的那些“自定义形状”、“特殊纹理投影”,在固定功能听上去就像笑话。
2.2 软件光栅化器的自由度
软件光栅化的思路就完全不同。它不是靠固定的硬件单元,而是在通用计算单元上写代码,把图元扫描转换的逻辑自己实现一遍。这里说的通用计算单元包括CPU的SIMD指令集,也包括GPU上的Compute Shader,或者是公司自研的ISP、DSP这类专用处理器。
软件光栅化最大的价值是自由。图元类型可以自定义,三角形、四边形、椭圆、点云、球体、任意微多边形,只要你能写出扫描算法,就能光栅化。精度可以随时切换,深度比较可以用浮点,可以用特殊的整数编码,甚至可以按需抖动。裁剪、覆盖测试、多采样这些都可以从底层重新实现,而不是只能开关硬件开关。
它的代价也很直观:慢,而且很多基础工作要自己重复造轮子。你需要自己维护图元缓冲区、自己设计线程调度和负载均衡策略、自己处理像素级原子操作的冲突。此外,软件光栅化器需要把颜色和深度写回缓冲区,如果多个线程同时写同一个像素,就涉及到原子性操作,处理不好会有明显的性能下降和渲染错误。
不过,软件光栅化有一个场景是硬件永远比不上的:当三角形小到一定程度时,硬件固定的图元设置开销是浪费的,而软件光栅化可以针对“极小图元”专门优化,用数学计算一次性覆盖大量像素。这个特点正是微多边形渲染能够成立的关键前提之一。
2.3 协同不是“二选一”,而是“任务级调度”
所以如果你问我,硬件光栅化和软件光栅化应该选哪个,我会给出一个听上去有点绕但非常实在的答案:都不是。现代渲染架构里真正有价值的不是非此即彼的选择,而是一个任务调度系统,它知道每个几何单元该被送去哪条光栅化路径。
举个例子。视口里有一堵墙,墙上有三万个三角形,每个三角形平均覆盖两三百个像素,这种负载交给硬件光栅化器几乎是白赚的性能。而墙角摆放着一堆复杂的装饰网格,经过自适应细分后产生了大量三四个像素甚至不到一个像素的微小面片,这些送入硬件光栅化器反而是自找麻烦,交给软件光栅化器在Compute上做像素级扫描处理,效率高出几个数量级。
软硬协同的核心工作,就是实时分析图元的“屏幕投影大小”,设定一个可配置阈值,然后把图元动态分配到硬件路径或者软件路径。这中间需要的是统一的数据结构、高效的调度逻辑和两类光栅化器之间的数据一致性保证。听起来工程量大,但这是微多边形时代不得不翻过的一座山。如果架构上没有这层协同逻辑,所谓微多边形渲染就只能停留在理论层面,因为局部极端密度的几何会把任何单一光栅化器的性能短板瞬间放大。
3. 微多边形+软硬协同:架构设计与实现路径
3.1 整体架构分层
我自己的实践里,会把这套架构从上到下拆成四层。
第一层是场景管理与几何压缩。CPU端负责场景遍历、视锥剔除、遮挡剔除,以及把原始网格数据压缩成适合后续处理的高效格式。这一层要尽可能早地干掉不可见的几何,减轻后面所有阶段的压力。
第二层是几何调度层。它负责把可见的网格拆成可独立处理的块,根据屏幕空间误差决定每个块的细分等级,再把它们分配到对应的光栅化路径。这一层是整个软硬协同架构里决策最密集的地方,也是“协同”二字真正的落脚点。
第三层是微多边形生成层。利用GPU Compute Shader或者CPU的并行指令,把需要细分的网格实时生成微面片。这一步的核心是“按显示屏空间误差驱动细分”,而不是无脑细分到某个固定级别。
第四层才是真正执行光栅化的执行层。它同时包含硬件光栅化路径和软件光栅化路径,按照调度层的指令执行。软件路径通常跑在GPU Compute上,因为数据已经在了显存里,CPU来回搬运反而是巨大的带宽浪费。
这四层之间,数据的“格式契约”一定要定义清楚。每个微多边形用什么结构表示、顶点数据放在哪、法线和UV怎么编码,这些一旦定了,后面所有层的优化空间就都被框出来了。
3.2 自适应细分的判据:屏幕上到底该有多密
自适应细分是这套架构里最需要“动脑子”的环节。很多第一次接触的人都问,到底怎么决定一个面片要细分多少次?
实用主义的角度看,最直接的判据是屏幕空间边长。你可以把世界空间的边长投影到屏幕上,算出对应的像素长度。如果像素长度大于某个阈值,比如2个像素,说明这个面片在屏幕上还不够密,可以继续细分;如果等于或者小于阈值,说明它已经足够小,可以当作点来光栅化了。
具体的投影计算也不复杂。给定一个世界空间边长L、一个距离摄像机的深度D、一个垂直方向的FOV角度,可以用这样一个近似公式估算屏幕像素边长:
屏幕像素边长 ≈ (L * viewport高度) / (2 * D * tan(FOV/2))
有了这个数值,做判据就变得非常直接。除了边长这种几何判据,还可以叠加材质复杂度的因素:表面有复杂法线细节或者高动态粗糙度的区域,即使边长已经比较小,也要继续细分一两次,避免明暗变化出现“切面感”;平坦、光滑的区域的细分阈值则可以放宽。
细分是实时进行的,不可能每帧对场景里所有网格做一遍。因此必须引入网格缓存机制。上一帧已经细分好的微多边形面片,只要模型没有发生大变动,下一帧可以直接复用缓存内容,只是要根据新的摄像机位置重新评估一下是否要调整密度。这个缓存机制如果做得好,整条细分链的实际开销可以控制在总帧时间的百分之十以内。
3.3 命令流与同步:软硬协同的基础设施
很多渲染工程师一看到“软硬协同”四个字就以为是指CPU和GPU并行计算,但实际工程里,更大的挑战在于协同双方的“通信成本”。这里有个很贴切的类比:Electron架构里,主进程和渲染进程之间用IPC传递消息,消息一次两次很便宜,但如果你在每一帧里塞几百条IPC消息,系统的开销就压不住了。CPU和GPU之间的通信也是这样,只不过通道换成了Command Buffer和Fence。
在设计协同架构时,我特别强调一个原则:不要事无巨细地同步。很多初版实现会倾向于让CPU告诉GPU“你现在要去光栅化这一批微多边形”,GPU每做完一步就回传一个Fence。这种过度同步会带来严重的管线停顿。正确的做法是让调度层在一帧开始时生成一批独立的渲染任务,把这些任务一次性提交给GPU,中间阶段用异步查询去获取状态,而不是每帧阻塞等待完成。
命令流的组织也需要推敲。硬光栅化和软光栅化的任务可以分到不同的Command Buffer中,在GPU端并行执行。如果你用Compute Shader实现软件光栅化,那软路径本身就等同于GPU内部的一次Dispatch,与硬件光栅化的Draw调用之间只需要插入一个Barrier,就能保证两个路径的深度和颜色数据一致。只要不盲目地在CPU和GPU之间来回跳动,协同的成本是可控的。
4. 实战:从引擎到应用的软硬协同落地
4.1 引擎里的软硬协同:Impeller、UE与WebGPU的实际选择
说完了架构,看几个真实世界的例子,你会发现这套思路其实已经悄悄渗透到很多引擎里了。
Flutter的Impeller渲染引擎是个很好的案例。早年的Flutter依赖Skia做软件渲染,后来为了性能大量启用GPU加速,但一直受到Shader运行时编译卡顿的困扰。Impeller的做法是在构建期把Shader预先编译好,运行时不带任何编译开销;同时把渲染管线定义为一系列可缓存的Pipeline状态对象,管线和命令流的生成方式与GPU硬件紧密协同。它用工程实践证明了“软硬协同”不一定是CPU和GPU的算法分工,也包括引擎构建期与运行期的分工、预编译与执行期的分工。
再看Unreal Engine的Nanite。UE5的Nanite在几何处理上采用的是GPU-Driven的完整流程:CPU只在最初做粗粒度的剔除,然后把所有网格数据留在GPU端,驱动Cluster化的LOD和光栅化路径。Nanite实际上同时维护了软件光栅化和硬件光栅化两条路径,依据图元在屏幕上的尺寸自动判断如何渲染。这个设计就是微多边形时代软硬协同架构在商业引擎里最完整的落地案例。
WebGPU的兴起也让这套思路在浏览器端成为可能。Web端过去受限于WebGL的固定流程,想做微多边形和自定义光栅化几乎不可能。WebGPU把Compute Shader和更灵活的管线状态带到了浏览器,意味着开发者现在可以在浏览器的GPU上运行自己的软件光栅化器,与传统的硬件光栅化器协同工作。这对3D网页渲染、在线模型编辑这类场景的影响会非常深远。
4.2 前端渲染场景里的软硬件分工视角
别以为这套架构只属于3D渲染。前端渲染的很多问题,本质上也是“哪部分活该交给浏览器引擎的固定流程,哪部分该交给开发者自己控制”。
比如用markdown-it渲染大量文字时,很多人会遇到页面卡顿。老实说,问题通常出在DOM节点数量过大和回流触发过于频繁。浏览器处理DOM的“光栅化”,本质上也是软硬件协同的结果:布局(Layout)在CPU端完成,绘制(Paint)在合成器与GPU之间分派。如果你不做任何优化,等于把“细分”和“调度”全部交给了浏览器默认策略;对海量文本,合理的方式是分批渲染、用虚拟列表控制可见DOM数量、或者干脆用Canvas自绘文本。
ECharts绘图闪烁也属于同一类问题。ECharts走的是Canvas绘制,Canvas的回流区域由浏览器合成器管理。闪烁往往和重绘范围判断、抗锯齿设置以及devicePixelRatio在retina屏幕上的处理有关。很多人直接改图表配置,但真正有效的是检查resize事件是否触发了一连串重复绘制,以及Canvas的坐标系统是否和物理像素对齐。硬件加速与输入事件处理之间的协同,这里被很多人忽略了。
还有个常被问的问题:DOM从上到下顺序渲染是不是更快?答案是浏览器大体上确实是从上到下依次做布局和绘制的,所以首屏CSS的顺序、占总高度比例很大的非关键元素,都会影响呈现速度。用content-visibility这类属性跳过屏幕外的渲染,就是在“软件调度层”干预浏览器的光栅化路径,换取更好的体验。
VSCode这类编辑器对大批量文本的处理也很有意思。早期VSCode用DOM渲染代码,后来引入WebGL加速特定场景;对超长文件依然需要虚拟列表。本质上它也是在“浏览器固定流程”和“自己控制的最小化DOM”之间做协同调度。这条思路和我们在3D渲染里探讨的软硬协同,是完全同构的。
4.3 实时云渲染:服务器端的软硬协同场景
把视角再拉高一层,说说实时云渲染。云端渲染服务器通常拥有多张专业GPU,云端接收终端输入,渲染出一帧画面,再编码成视频流推给端侧。在这个链路里,软硬协同的体现非常明显:服务器端的CPU负责场景加载、物理模拟、用户交互逻辑;GPU负责光栅化和计算着色器;硬件编码器负责视频压缩;少量软件编码路径作为备选或特殊场景回退。
对云渲染项目来说,最怕的不是GPU不够快,而是端到端延迟不可控。每一帧的调度都要尽可能减少同步点,尽量让渲染任务和编码任务流水化。我见过不少云产品在初期把渲染和编码串行执行,每一帧的编码都要等渲染完全结束,硬生生把管线延迟抬了一倍。优化的方式就是把编码器改为异步模式,让编码和下一帧的渲染重叠。这种“让两件原本串行的硬件任务并行起来”的思路,正是软硬协同最适合解决的问题。
云渲染里图形与编码之间的数据格式也要精心设计。直接让编码器去读光栅化后的RGBA缓冲,带宽和延迟都不理想;通常的处理是把渲染结果写入专用显存缓冲,再用零拷贝或DMA的方式交给编码器。这里每一步都是“协同”的细节,每一步也都能成为性能的关键瓶颈。
5. 常见问题与排障速查表
5.1 微多边形时代架构的常见问题实录
下面这张表是我这几年在各类型项目里遇到频率最高的渲染问题整理出来的。覆盖了从引擎层到应用层的主要故障,也保留了排查的核心思路。
| 问题现象 | 可能原因 | 排查方向 | 解决建议 |
|---|---|---|---|
| 微多边形细分后帧率骤降 | 屏幕空间边长阈值过小,生成几何量超出了管线处理能力 | 检查细分统计计数和顶点吞吐量 | 适当调大细分阈值,增加缓存复用 |
| 软硬切换时出现跳帧 | 调度层频繁在两条光栅化路径之间切换,产生管线停顿 | 查看命令流的Barrier数量和切换频率 | 为切换增加缓冲阈值,延迟切换决策 |
| Shader首次加载卡顿 | 运行时Shader编译造成管线阻塞 | 检查帧率曲线上的单点尖峰 | 改用构建期预编译,或异步编译 |
| ECharts绘图闪烁 | Canvas重绘范围判断异常或devicePixelRatio导致采样错位 | 检查resize事件触发次数和Canvas尺寸 | 规范化resize监听,对齐物理像素坐标系 |
| 海量markdown/DOM文本卡顿 | DOM节点过多、布局频繁回流 | 观察DOM节点数和Frame中Layout占比 | 虚拟列表、分批渲染,必要时Canvas自绘 |
| Unity中SpriteRenderer出现在模型前方 | 渲染队列与深度测试排序冲突 | 检查材质RenderQueue和ZWrite设置 | 调整渲染队列和深度写入开关 |
| VS Code渲染大文件缓慢 | DOM节点数量随文件大小线性增长 | 查看文件对应的虚拟列表范围 | 启用虚拟化渲染,避免全量DOM生成 |
| 云渲染视频流卡顿 | 渲染与编码之间存在不合理的同步 | 分析编码器等待时间和管线深度 | 启用异步编码流水线,增加输入缓冲 |
5.2 独家避坑心得
最后说几个常规文档里看不到的工程细节。第一点,软件光栅化路径尽量不要独占整个GPU。你用Compute Shader做软光栅化,它本质上也是GPU上的消耗者,如果配置不当,会和硬件光栅化路径抢Shader单元和显存带宽,结果协同没协同成,反而两边都慢了。合理的做法是限定软路径的线程块数量,给硬路径留足余量。
第二点,命令流里的Fence不要每个任务都插。Fence的插入本身就是一次同步开销,插多了整个管线就变成了“串行执行”。我更建议的做法是按“帧”为单位插入Fence,最多在CPU需要读取GPU结果时才临时插入,用完立即去掉。这样既保证了数据一致,又不会把管线的并行度毁掉。
第三点,也是我踩得最深的一个坑:自适应细分之后,很多美术资源里的法线贴图、UV接缝会被放大得极其明显。微多边形把几何密度提上去了,同时也把纹理采样频率提上去了,原本不那么明显的接缝和压缩伪影会在特写镜头里直接暴露。这个问题不是渲染器层面的bug,而是制作流程要跟着升级。做微多边形渲染,纹理的分辨率、接缝的放置方式、法线贴图的烘焙方式,都得按“极高细节”的标准重新做一遍。这个坑当时让我们整个团队花了两周才意识到根因所在。
这套架构说到底并不神秘:它是把过去固定死的光栅化流程,拆解成了可调度、可选择、可编程的微几何流水线。真正难的不是某一个光栅化器写得好,而是调度层怎么把对的任务交给对的执行单元。这个思路在未来很长时间内都会是渲染引擎设计的主线。