第 21 章
着色器的巴别塔
表达自由的每一次扩张,都在暗中标好了跨平台兼容性的价格。这个悖论在2017年之前还只是渲染工程师圈子里的技术牢骚——一个材质在DirectX上完美运行、在OpenGL上出现精度偏差、在移动端GPU上直接编译失败,这类问题被当作跨平台开发的日常摩擦,而非架构危机。但到了2017年之后,三股力量的汇流将这种摩擦升级为一场足以让整个构建管线停摆的结构性撕裂。
把时间拨回2015年,当DirectX 12、Vulkan和Metal这三套现代图形API相继完成首次公开亮相时,业界普遍沉浸在一种技术乐观主义之中。更接近硬件的底层访问权限、更精细的显存控制、更灵活的资源绑定方式——这些特性被理解为释放GPU全部潜能的钥匙。几乎没有人公开讨论一个后果:这三套API正在用三种互不兼容的语法,描述同一个物理芯片的行为。要理解这场危机的本质,需要先理解一个基本事实:GPU从来不是通用计算单元。
尽管2015年之后的营销话语热衷于将GPU描绘为高度并行的处理器阵列,但在指令集架构层面,每一家GPU厂商的产品都是一座孤岛。NVIDIA的CUDA核心、AMD的流处理器、Imagination Technologies的PowerVR架构、Arm的Mali系列、高通的Adreno系列——这些芯片的底层指令集不仅互不兼容,而且在同一厂商的不同代际之间也存在断裂。DirectX 11时代,驱动程序的职责之一就是在HLSL着色器代码和GPU指令集之间充当翻译层。
这个翻译层消耗了可观的CPU时间,但它至少保证了一份HLSL代码可以在不同硬件上运行,而无需开发者关心底层指令集的差异。DirectX 12、Vulkan和Metal的共同设计目标之一,就是把这个翻译层从驱动程序中剥离出去。它们的工程逻辑是清晰的:驱动程序不再需要在运行时猜测开发者的意图,开发者可以在构建阶段完成着色器编译,从而消除运行时的性能抖动。
但这个决策有一个未被充分讨论的社会学后果:它把硬件碎片化的全部重量,从GPU厂商的驱动团队肩上卸下来,压到了引擎开发者的肩上。过去,当一款新GPU发布时,是NVIDIA或AMD的驱动工程师负责确保上一代游戏中的着色器代码能在新硬件上正确运行。现在,这个责任转移到了引擎开发商——他们需要在构建阶段为每一款目标GPU生成独立的着色器变体,并验证每一个变体的正确性。
Epic的渲染团队是最早感受到这股重量的人群之一。2014年UE4发布时,引擎的材质系统已经完成了一次关键架构转型:材质不再是一组固定参数的集合,而是一个基于节点的可视化编程环境。技术美术可以在材质编辑器中拖拽节点——纹理采样、向量运算、材质函数调用——构建出极其复杂的表面效果。在编辑器内部,这个节点图被编译成一套中间表示,然后再翻译成目标平台的着色器代码。
UE4发布初期,这套系统的目标平台主要是PC上的DirectX 11,以及PlayStation 4和Xbox One各自定制化的GPU指令集。每个平台需要一套独立的着色器变体,但平台数量是有限的,变体爆炸还处于可控范围。到了2017年,这个局面被三股力量同时打破。
第一股力量是移动端的崛起。UE4在2014年就已经支持移动端预览,但直到2016至2017年间,随着《堡垒之夜》跨平台战略的推进,Epic才真正开始严肃对待移动端着色器编译管线。移动端GPU的碎片化程度远超PC和主机:同一代Android设备可能搭载来自高通、Arm、Imagination、三星等不同厂商的GPU,每个厂商的指令集架构和精度处理方式都不相同。一个在PC上通过材质节点图生成的着色器,在移动端编译时可能因为精度差异产生视觉瑕疵——高光计算溢出、法线贴图精度丢失、半透明混合顺序错误——或者更糟,直接编译失败,在屏幕上留下一片品红色。
品红色是Unreal Engine用来标记材质编译失败的默认颜色。在引擎内部,当一个材质的着色器无法在目标平台上成功编译时,渲染器会在该材质应该出现的位置填充这种刺眼的洋红色。对于技术美术来说,品红色是一种沉默的判定:你在节点编辑器里精心构建的逻辑,在另一个平台上被判定为不可翻译的方言。这种体验的残酷之处在于,技术美术在PC编辑器中看到的一切都是完美的——光照准确、纹理清晰、材质函数按预期执行——但当项目被部署到移动设备上时,某些材质突然变成了一片刺眼的品红,没有任何中间状态,没有任何渐进式的降级,只有成功或失败两种结果。
第二股力量是主机世代的更替。2016年底PlayStation 4 Pro发布,2017年Xbox One X发布,这两款半代升级主机引入了新的GPU特性,但它们的着色器编译需求与基础型号并不完全一致。这意味着,一个面向PlayStation 4开发的UE4项目,在Pro型号上需要生成额外的着色器变体。
2017年也是任天堂Switch的发布年份,这款设备的NVIDIA Tegra X1芯片带来了另一套完全不同的GPU指令集。到2018年,一个面向全平台发布的UE4项目,理论上需要为至少六到八个不同的GPU目标生成着色器变体:PC上的DirectX 11和DirectX 12、PlayStation 4基础版和Pro版、Xbox One基础版和X版、Switch、以及iOS和Android的移动端GPU阵列。每一个目标平台都是一个独立的编译维度,而每个维度上的着色器变体都需要被独立验证。第三股力量来自引擎内部。UE4的材质节点图系统在2014至2017年间经历了快速的功能膨胀。静态开关节点的引入允许材质在不同条件下选择不同的计算路径,但这个节点的本质是在编译时生成多条着色器分支。一个包含三个静态开关的材质节点图,理论上可以生成八套不同的着色器变体。
当技术美术在材质图中嵌套材质函数、使用材质图层、启用自适应曲面细分时,着色器排列组合的数量以阶乘级增长。Epic内部在2017年前后观察到,某些复杂材质的着色器变体数量开始突破三位数。一个节点图,数百个着色器排列。而每一个排列都需要在每一个目标平台上独立编译。这就是着色器巴别塔危机的数学本质:材质创作的自由度与目标平台的广度,在物理上不可兼得,因为编译时间的增长是乘法关系。
总编译成本等于材质数量乘以每个材质的变体数量乘以目标平台数量。在2014年,这个公式的三项因子都还处于低位——材质数量以百计,每个材质的变体数以十计,目标平台以三到五个计。到了2017年,三项因子同时膨胀:材质数量在大型项目中突破千级,复杂材质的变体数量突破百级,目标平台数量突破八到十个。总编译成本开始以指数级攀升。2018年前后,一些大型UE4项目的完整着色器编译时间已经突破了单台构建服务器可以承受的阈值,迫使团队部署分布式编译集群来分担负载。
Epic对这一危机的应对,不是试图减少着色器变体的数量——变体数量是由平台碎片化和材质复杂度共同决定的,而Epic对这两个因素都没有完全的控制权——而是重构着色器编译管线本身,使其能够以更高的效率处理变体爆炸。这个重构的核心,是一套中间表示系统。这套系统的工作逻辑可以用一个语言学比喻来理解。
面对一座人人说着不同方言的巴别塔,Epic的选择不是教所有人说同一种语言——那需要GPU厂商达成指令集层面的统一,而这在商业和技术上都不可行——而是训练一群精通所有方言的翻译官。在UE4的编译管线中,材质节点图首先被翻译成一种引擎内部的中间表示,这种中间表示与任何特定的GPU指令集无关。然后,针对每一个目标平台,编译栈的后端将这份中间表示翻译成该平台的着色器方言:HLSL for DirectX、GLSL for OpenGL和Vulkan、Metal Shading Language for Apple平台、以及各种移动端GPU的专有方言。
这套翻译官系统的工程代价是巨大的。每一个后端翻译器都需要精确理解目标平台的GPU指令集特性:哪些运算可以用原生指令高效执行,哪些运算需要软件模拟,哪些精度转换是安全的,哪些会导致视觉差异。Epic的渲染工程师需要在数百种GPU型号上验证着色器编译结果的正确性和性能特征。这是一项没有终点的维护工作,因为GPU厂商每年都在发布新的架构、新的指令集扩展、新的驱动版本。每一个新变量的引入,都意味着所有已知的着色器排列需要重新测试,所有后端翻译器需要重新验证。
2018年3月,Epic在GDC上公开了UE4.19的着色器编译改进。在演讲中,渲染工程师承认着色器编译管线已经成为引擎开发中最消耗工程资源的模块之一,其复杂度甚至超过了渲染器本身的核心算法。这不是因为着色器编译在算法上有多深奥——编译器的基本逻辑在计算机科学中已经研究了几十年——而是因为需要适配的目标平台数量已经膨胀到一个令人窒息的程度。
每一个新平台的加入,每一个新GPU架构的发布,都意味着编译栈的所有后端翻译器需要重新验证,所有已知的着色器排列需要重新测试。这是一场没有终点的军备竞赛,而竞赛的对手不是任何一家竞争对手,而是硬件碎片化本身。
在Epic为UE4的着色器编译栈投入大量工程资源的同时,Unity也在经历一场属于自己的着色器危机。这场危机的起点,是Unity在2015至2016年间做出的一项关键架构决策:可编程渲染管线。在Unity 5时代,引擎的渲染管线是一个内置的、相对封闭的系统。材质着色器由Unity内部生成,开发者可以通过ShaderLab语言编写自定义着色器,但渲染管线的整体架构——前向渲染还是延迟渲染、光照计算的流程、阴影生成的算法——是由Unity决定的。这种封闭性在移动端是一个优势:Unity可以针对每个平台预优化着色器生成逻辑,确保编译出来的着色器变体在目标设备上能够高效运行。
Unity Asset Store自2010年推出以来,截至2018年累计下载量约四千万次,其中大量资产是依赖这套内置管线运行的。这个生态的稳定性建立在着色器编译路径的可预测性之上。但在2015年前后,高端PC和主机开发者开始对这套封闭架构表达不满。他们的需求很明确:如果要实现与UE4同级别的视觉效果,就需要对渲染管线进行深度定制。集群光照、屏幕空间反射、基于物理的渲染的特殊路径——这些技术无法简单地通过替换几个着色器来实现,它们需要修改渲染管线本身的执行逻辑。开发者希望控制光照计算的每一个步骤,希望在渲染管线的任意阶段插入自定义逻辑,希望使用自己的剔除算法和阴影生成策略。
Unity的回应是SRP——可编程渲染管线。这套系统在2017年随Unity 2017.1版本首次公开,2018年随Unity 2018.1进入正式可用状态。SRP的核心思想是将渲染管线从引擎内核中剥离出来,作为一套可以由开发者用C#脚本完全定制的模块。
Unity提供了两套参考实现——面向高端PC和主机的高清渲染管线(HDRP)和面向移动端的轻量级渲染管线(LWRP,后更名为通用渲染管线URP)——但开发者可以从零开始构建自己的渲染管线。SRP的发布在技术社区引发了复杂的反应。一方面,它赋予了开发者前所未有的控制权:一个技术美术现在可以决定光照计算的每一个步骤,可以插入自定义的渲染Pass,可以在渲染管线的任意阶段注入自定义逻辑。
另一方面,它把着色器编译的复杂性直接暴露给了开发者。在旧的内置管线中,当开发者通过ShaderLab编写一个自定义着色器时,Unity的内部系统会自动处理跨平台编译的细节。但在SRP架构下,开发者需要自己管理着色器变体的生成逻辑,自己处理不同平台之间的编译差异。控制权的代价是责任——Unity将着色器编译的一部分负担从引擎内部转移到了开发团队身上。
更复杂的是,SRP引入了Shader Graph——一个与UE4材质节点图类似的视觉化着色器编辑器,于2018年首次发布。Shader Graph让艺术家可以用拖拽节点的方式创建材质效果,无需手写ShaderLab或HLSL代码。但这个便利是有代价的:Shader Graph生成的着色器代码需要经过两层翻译——先从节点图翻译成HLSL中间表示,再从HLSL翻译成目标平台的着色器方言。每一层翻译都可能引入精度损失、逻辑偏差或编译失败。当翻译链中的任何一个环节出错时,艺术家的节点图在目标平台上变成品红色,而理解这个失败需要穿透两层翻译的抽象层,追溯到原始节点逻辑与目标GPU指令集之间的根本性不兼容。
2019年前后,Unity社区中开始出现关于SRP着色器编译问题的讨论帖。一个反复出现的案例模式是:开发者在HDRP中创建了一个使用Shader Graph的材质,在PC编辑器中预览一切正常,但构建到iOS设备后,材质变成了品红色。
排查后发现,问题出在Metal着色器编译器对某个HLSL内置函数的处理方式与DirectX不同——该函数在DirectX中返回浮点值,在Metal中返回半精度值,精度差异导致材质逻辑的某个分支条件判断错误。修复这个问题需要在Shader Graph中手动插入精度转换节点,而这要求开发者理解Metal着色语言的精度模型——一项Shader Graph本应帮助艺术家绕过的底层知识。这正是着色器巴别塔危机最尖锐的表现形式:引擎试图让艺术家用同一种语言创作材质,但底层平台的方言差异是如此根本,以至于这个翻译层在物理上无法做到完全透明。当翻译失败时,艺术家面对的不是一个可以调试的错误信息——他们看到的是品红色,一种宣告表达式在此地不可理解的沉默。
Unity SRP的另一个结构性挑战,是自定义渲染管线与Asset Store生态之间的兼容性断裂。Asset Store上大量依赖内置管线的着色器资产,在SRP架构下无法直接使用。
开发者要么等待资产作者发布SRP兼容版本,要么自己手动将旧着色器移植到新的管线架构中。这个迁移成本在2018至2020年间成为Unity社区的一个持续性痛点,也暴露了引擎架构转型中的一个根本困境:开放性的代价是生态碎片化。当Unity将渲染管线的控制权交给开发者时,它同时也打破了Asset Store赖以运转的兼容性契约——一个在2015年购买的着色器资产,在2019年的SRP项目中可能完全无法使用。
在Epic和Unity围绕着色器编译问题各自修筑翻译层的同时,id Software走了一条完全不同的路。id Tech引擎在2016年《毁灭战士》中展示了其标志性的渲染能力,但id Tech 6的着色器系统遵循着一个与Unreal和Unity截然不同的哲学:它不为通用性而设计。id Tech 6的着色器编译管线是围绕一个非常狭窄的目标平台集合构建的——PC和当时的两款主流主机。
引擎的材质系统不提供UE4材质图或Unity Shader Graph那样的通用节点编辑器,而是依赖于一套高度优化的预编译着色器库。技术美术可以调整材质参数,但无法在运行时动态生成新的着色器逻辑。这种设计牺牲了材质创作的灵活性,换取了编译时间的可控性和运行时的稳定性。在《毁灭战士》的开发周期中,id Software的渲染团队不需要处理数百个着色器变体的跨平台编译验证问题,因为着色器的数量是预先确定的,编译路径是手工维护的。每一个着色器变体都是经过人工审查的,它的性能特征和跨平台行为是已知的、可预测的。
2018年《毁灭战士:永恒》开发期间,id Tech 7引入了更多可编程特性,但引擎的着色器系统仍然保持着这种预编译库的架构哲学。id Software的立场在是对巴别塔危机的一种激进回应:如果兼容所有平台的代价是构建一个永无止境的翻译层,那么也许更好的选择是限制材质系统的表达范围,让它在更少的目标平台上运行得更可靠。
这不是技术能力的不足——id Software的渲染工程师完全有能力构建一个通用的跨平台编译栈——而是一种有意识的架构选择,一种对着色器巴别塔危机的战略性撤退。
这三条路线——Epic的通用翻译层、Unity的可编程管线加Shader Graph、id Software的预编译库——在2017至2022年间并行演进,它们代表了引擎架构对同一个结构性矛盾的不同回应。但没有任何一条路线能够真正解决这个矛盾。Epic的翻译层在不断膨胀,每增加一个目标平台、每发布一款新GPU,都需要更新后端翻译器。Unity的SRP架构赋予了开发者控制权,但也把着色器编译的复杂性转移到了开发团队身上,同时打破了Asset Store的兼容性契约。id Software的预编译库保持了稳定性,但代价是放弃了跨平台通用化的野心,将引擎的使用范围限制在那些愿意接受材质系统约束的工作室。
2020年之后,一个新的因素开始加剧这场危机:着色器编译时间正在从工程不便演变为生产瓶颈。在2014年,一个典型3A项目的完整着色器编译可能需要几十分钟。到2018年,这个时间增长到数小时。到2021年前后,一些大型项目的着色器编译时间开始以天为单位计算。这不是因为GPU变得更慢了——恰恰相反,GPU性能在持续增长——而是因为着色器变体的数量增长超过了编译工具链的处理能力。
着色器变体爆炸的驱动因素之一是材质节点图的复杂度增长。在UE4的材质编辑器中,一个技术美术可以轻松构建包含数十个纹理采样、数百个算术运算的复杂材质。当这个材质被标记为用于所有平台时,引擎的编译系统需要为每一个平台生成一份独立的着色器代码。
如果材质中包含静态开关,变体数量会成倍增长。如果一个游戏包含上千个这样的材质——这在3A项目中并不罕见——总变体数量可以轻松突破十万级别。每一个变体都需要被编译、被验证、被缓存。编译时间从分钟级滑向小时级,再从小时级滑向天级。
另一个驱动因素是平台数量的持续增长。2017年之后,除了传统的PC、主机和移动端之外,新的平台类别开始出现:任天堂Switch引入了混合形态的GPU需求,VR头显带来了高帧率、低延迟的着色器编译要求,云游戏平台需要着色器在数据中心GPU上高效运行。每一个新平台都意味着编译栈需要支持一套新的GPU指令集或驱动特性,每一个新特性都意味着所有已知着色器变体需要重新验证跨平台行为。
2022年,Epic在UE5的文档中公开了一个令人警醒的数字:一个典型的UE5项目可能生成超过十万个着色器变体。这些变体需要在所有目标平台上编译和验证,而编译过程中的任何一个失败——哪怕只是十万分之一——都可能导致游戏中某个材质在特定设备上渲染为品红色。Epic的应对措施是持续投资着色器编译基础设施:改进编译缓存机制、引入增量编译、优化中间表示的后端翻译效率。但这些措施本质上是在管理复杂性,而不是消除复杂性。
编译缓存可以避免重复编译相同的着色器变体,但它不能减少变体的总数。增量编译可以只重新编译修改过的材质,但当修改涉及一个被数百个材质引用的材质函数时,级联重编译仍然不可避免。
Unity在2021至2022年间也在进行类似的调整。URP的着色器编译管线经过了多次重构,目标是减少默认生成的着色器变体数量,同时提供更清晰的工具让开发者自己控制变体生成逻辑。但Unity面临的挑战比Epic更复杂,因为Unity的开发者群体更加碎片化:从使用内置管线的移动端小团队,到基于HDRP构建高端PC游戏的工作室,再到用URP覆盖全平台的跨平台项目——每个群体的着色器编译需求都不同,而Unity需要为所有这些群体提供可用的工具。一个面向移动端休闲游戏的开发者可能只需要几十个简单的着色器变体,而一个使用HDRP的PC游戏项目可能需要数万个。Unity的架构必须同时容纳这两种极端情况,以及它们之间的所有中间状态。
2023年8月,Unity中国宣布推出团结引擎。这个基于Unity 2022 LTS的分支版本,在官方公告中列出了一系列针对中国本土平台的支持:微信小游戏、OpenHarmony、AliOS。在技术细节中,有一项说明揭示了着色器巴别塔危机在2023年的最新演变:团结引擎的着色器编译管线需要为这些平台生成独立的着色器变体。微信小游戏运行在WebGL环境中,WebGL的着色器规范基于OpenGL ES的一个子集,与标准Unity管线支持的GLSL方言存在差异。OpenHarmony的图形栈使用自研的渲染架构,其着色器编译路径与标准Android设备不同。
AliOS作为面向车载和嵌入式的操作系统,其GPU驱动模型又引入了另一套变量。这意味着,一个通过Shader Graph创建的材质节点图,在团结引擎中需要被翻译成比标准Unity更多种的方言。翻译层的复杂度不是线性增长的——每增加一种方言,就需要验证它与所有已有方言之间的互译关系。
如果标准Unity支持十种目标平台着色器方言,团结引擎新增三种,那么需要维护的翻译关系不是从十增加到十三,而是从四十五种两两组合增加到七十八种。这是组合数学的残酷逻辑,它不关心商业需求或工程意愿,它只执行乘法的冷酷法则。
着色器巴别塔危机的历史后果,在2022年前后已经清晰可辨。引擎的角色从一个能力的提供者转变为复杂性的管理者。在2014年,引擎的核心任务是暴露更多GPU功能给开发者:新的着色器模型、新的纹理格式、新的光照算法。到2022年,引擎的核心任务变成了在暴露与封装之间找到那个脆弱的平衡点:暴露太少,开发者无法实现想要的视觉效果;暴露太多,着色器编译的复杂性将压垮生产管线。这个转变不是修辞层面的——它体现在工程资源的分配上。渲染新特性的开发团队规模在收缩,而着色器编译基础设施的维护团队在扩张。这个转变的标志之一,是引擎厂商开始投入大量工程资源构建着色器编译的验证和调试工具。
UE5引入了着色器编译的详细诊断面板,允许开发者追踪每一个着色器变体的编译状态、编译时间和目标平台。Unity在2022年发布了Shader Graph的调试工具集,帮助开发者定位跨平台编译中的精度差异和逻辑错误。这些工具的存在本身,就是对着色器巴别塔危机的一种承认:翻译层不可能做到完全透明,所以开发者需要工具来理解翻译过程中发生了什么。工具不是为了消除翻译失败,而是为了让失败变得可诊断。
另一个标志是,行业开始出现对着色器复杂度进行主动限制的实践。一些大型工作室在2020年之后制定了内部的着色器编码规范,限制单个材质的节点数量、禁止某些在移动端容易出问题的运算类型、要求所有材质必须通过特定目标平台的编译验证才能提交到版本库。这些规范的本质,是在材质创作的自由度上主动让渡一部分,以换取跨平台兼容性的确定性。技术美术在节点编辑器里获得的表达自由,正在被跨平台编译的碎片化反噬——而工作室的制度回应,是用规则约束这种自由。
这恰恰是本章论断的核心:材质创作的自由度与目标平台的广度,在物理上不可兼得。引擎可以构建越来越复杂的翻译层来延缓这个矛盾的爆发,但翻译层本身也会成为复杂性的新来源。当一个材质节点图在PC上完美运行、在移动端变成品红色时,问题不在于翻译层的质量不够好——问题在于,让同一种材质逻辑在指令集架构完全不同的GPU上产生完全相同的渲染结果,这个目标本身在物理上是无法完美实现的。浮点精度的差异、纹理采样方式的差异、深度缓冲处理方式的差异——这些差异不是bug,它们是不同GPU设计的必然产物。翻译层可以缩小差异,但无法消除差异。
引擎工程师可以在DirectX和Metal之间建立映射表,将HLSL的内置函数翻译成Metal Shading Language的等价函数,但当两个平台的浮点运算采用不同的舍入模式时,当纹理采样的各向异性算法在硬件层面存在差异时,当深度缓冲的精度在不同GPU上使用不同的内部表示时,翻译层能做到的最好结果不是完全一致,而是视觉上可接受。
2017至2022年间的着色器巴别塔危机,最终没有一个胜利者。Epic没有找到大一统的着色器语言,Unity没有让Shader Graph做到完全的跨平台透明,id Software没有说服行业放弃通用化的野心。三方都在各自的路径上修筑翻译层,每一层翻译都解决了一部分问题,同时也制造了新的维护负担。到2022年底,一个典型的商业引擎的着色器编译栈,其代码量和维护成本已经超过了引擎中任何其他单一模块。这不是技术理想主义的胜利,这是工程实用主义对一个不可解问题的务实回应。
着色器编译时间在2022年的大型项目中已经占到完整构建时间的显著比例。在某些案例中,一个项目的着色器变体总数突破了三十万,全平台编译需要超过四十八小时的服务器时间。当构建服务器在深夜轰鸣,逐行翻译着从节点图衍生出的数十万种着色器排列时,这座巴别塔的重量不是抽象的隐喻——它是电费账单、是发布延期的风险、是某个技术美术在移动端预览中看到的那片品红色。引擎纪元的下一个压力点,正在这些数字的堆积中酝酿成形。