阴影管线工程 2026:从 CSM 到虚拟阴影贴图
把阴影当管线治理而不是当一张贴图处理:从深度偏置与 texel snapping 的稳定性根源,到级联划分的静态区间,再到虚拟阴影贴图的按需分页与缓存失效,拆解现代引擎阴影管线的完整工程决策链。
约 34 分钟阅读9,958 字5 次阅读博主

把阴影当管线治理而不是当一张贴图处理:从深度偏置与 texel snapping 的稳定性根源,到级联划分的静态区间,再到虚拟阴影贴图的按需分页与缓存失效,拆解现代引擎阴影管线的完整工程决策链。

把阴影当管线治理,而不是当一张贴图处理:从深度偏置与 texel snapping 的稳定性根源,到级联划分的静态区间与混合缝,再到虚拟阴影贴图的按需分页与缓存失效,本文拆解现代引擎阴影管线的完整工程决策链。
在任何实时渲染管线的开销清单里,阴影都排在最前面,而且理由和其他特效完全不同。Bloom、景深、泛光这类效果做错十个百分比,玩家大概率说不出来;但阴影做错了,玩家会立刻说「这画面不对」——影子悬空、影子闪烁、影子消失,全部都是几何层面的谎言,而人眼对几何谎言的敏感度远高于对颜色和亮度的敏感度。这就是本文标题里「正确性」三个字的分量:阴影不是画质选项,是场景可信度的地基。
代价同样在前排。阴影贴图的本质是把场景从光源视角再渲染一遍,而且往往不止一遍:一个方向光按级联拆成四张,十几个点光源各自再来一张立方体贴图,在重场景里,阴影 pass 的三角形提交量经常追平甚至超过主视角。引擎开发者常犯的一个方向性错误,是把阴影当成「材质系统的一个附属功能」,在材质参数里调偏置、在光照组件里调分辨率;而真正成熟的引擎——不管自研还是 Unreal Engine 5、Godot 这样的开源引擎——都把阴影组织成一条独立管线:光源筛选、投影矩阵计算、深度 pass、页管理、采样过滤、调试可视化,各有各的模块和预算。把这条管线拆开来看工程决策,是本文的主线。
还有一层原因:阴影技术的选型窗口非常窄。1978 年 Williams 提出阴影贴图,微软在 DirectX 技术文章里回顾这近半个世纪的演化时也承认,闪烁边缘、透视走样、精度问题至今仍然存在。算法骨架没变过——第一遍从光源渲染深度,第二遍拿着片元的光源空间深度去比较——但围绕这个骨架的工程方案层峦叠嶂:偏置怎么给、级联怎么切、贴图怎么稳定、虚拟化怎么分页,每一步都是可以写一章的决策。2026 年回头看,主流方案已经收敛成两条路线:以级联阴影贴图(CSM)为代表的传统路线,和以虚拟阴影贴图(VSM,Virtual Shadow Maps)为代表的虚拟化路线。本文就把这两条路线的每一个工程节点摊开。
要把阴影管线做对,第一步是把「阴影不好看」拆成可以定位的误差类别。从光源到屏幕,一个片元的深度要走完这条链:世界空间坐标变换到光源裁剪空间,光栅化量化到阴影贴图的具体纹素,采样时再从着色像素重投影回这张贴图取深度做比较。误差就在这条链的每一级产生,微软的技术文章把它归纳成几类走样,这里按工程归因重新分一层。
第一类是分辨率失配,也就是走样(aliasing)。近处的一批着色像素落到同一颗阴影纹素上,阴影边缘就出现大块锯齿;微软文章称其为透视走样(perspective aliasing)——因为摄像机近处的像素密度天然高于远处,而阴影贴图的纹素密度是均匀的,两者比值沿着视深变化。另一类容易被混淆的是投射走样(projective aliasing):即使像素与纹素恰好一一对应,当几何表面的切平面与光线方向趋于平行时,表面在光源视角下被极端压缩,同样一片表面只摊到几颗纹素,斜坡上的阴影就会脏。这两种走样的解法方向完全不同:透视走样靠重新分配分辨率(级联、透视投影扭曲),投射走样靠偏置和过滤兜底,混在一起谈是新人排查阴影问题最常见的死胡同。
第二类是精度误差。深度缓冲是有限位数(16/24/32 位)的定点或浮点分布,近远平面的比值直接决定深度值的分布密度;光栅器插值出的深度和着色器计算出的深度来自两条不同的计算路径,本身就带偏差。当片元深度和纹素深度几乎相等时,比较结果就是随机的——这正是阴影粉刺(shadow acne)的成因之一:整个表面本该被照亮,却因为量化误差随机地自遮挡出莫尔斑点。
第三类是采样与过滤误差:单次深度比较是二值的,阴影边缘没有半影,于是需要百分比渐进过滤(PCF)一类的多次采样;而采样核的大小、旋转与抖动又引入自己的噪声模式。最后还有一类不算误差但常被误判的问题:语义边界——哪些物体投射阴影、哪些接收、哪些只接收不投射,是引擎语义层的决策,出问题时表现和走样一模一样,先查它再查算法能省一半时间。
还有一个贯穿全文的不对称性值得记住:偏置给少了,得到的是满屏粉刺(假阴影);偏置给多了,得到的是影子悬空(Peter Panning,假光照)。前者灾难大、后者灾难小,所以整个偏置工程的调参方向是「宁可悬空、不可粉刺」,但悬空到影子脱离物体也是废——工程空间就在这两堵墙之间,而撑宽这个空间的手段(法线偏置、紧凑近平远面、反向渲染背面)就是第三节的内容。
偏置是所有阴影管线里第一个需要严肃对待的参数组,也是最依赖原理理解的。微软深度贴图文章给的基准方案有两层:一层是常数深度偏置,直接把片元在光源空间的深度加一个固定值;一层是斜率缩放偏置(slope-scaled depth bias),Direct3D 10 以来的硬件光栅器原生支持,按多边形相对光线的坡度缩放偏置量——正对光的表面几乎不加偏置,与光线几乎平行的表面加很大的偏置。原理正是第二节的投射走样:坡度越大,同一片表面被压缩得越狠,量化误差越大,需要的补偿越多。两个系数组合使用是所有引擎的默认形态,但「默认能跑」和「调对了」之间隔着大量场景相关的工作。
斜率偏置的边界情况值得单独说。一是它与法线贴图的冲突:偏置补偿的是几何坡度,而法线贴图改变的是着色法线,带法线贴图的表面在阴影判定时用的是几何深度,着色时用的是扰动法线,两者不一致会在凹凸细节上产生闪烁。二是它与半透明和 alpha 测试材质的冲突:裁剪树叶片元时,斜率偏置按整个三角形算,细节边缘照样粉刺。这些没有银弹,工程上的做法是给这类材质单列偏置系数或单独的阴影 pass 配置。
比深度偏置更现代、也更稳健的是法线偏置(normal offset):在着色时把接收点的世界坐标沿几何法线抬升一段距离再去做阴影判定,抬升量通常与阴影纹素的世界尺寸成正比。它等效于把「深度比较」的参考面沿法线方向推开,而不是沿光源方向硬拉深度值,因此对斜坡的适应性显著更好,很多现代引擎把它作为主偏置手段、把深度偏置降级为辅助。工程上要注意两件事:抬升量必须随级联分辨率变化——近处级联纹素小、抬升要小,远处级联纹素大、抬升要大,用统一常数必然一头粉刺一头悬空;以及抬升方向用几何法线而不是着色法线,否则法线贴图的凹槽会自己把自己「抬」进阴影。
深度精度的另一半功夫在投影矩阵本身。微软文章用相当长的篇幅论证近平远面要尽可能紧:AABB 式取整个场景包围盒最省事但最浪费精度,最坏情况深度范围四倍于必要值;正确做法是把光源正交视锥与场景几何相交,用裁剪后的最小最大深度做近平远面,DirectX SDK 的 CascadedShadowMaps11 示例里的 ComputeNearAndFar 函数演示了完整的三角形裁剪实现。近平远面收紧到什么程度有硬指标:紧到 16 位深度缓冲都够用时,显存和带宽同时受益。另一个精度手段是渲染背面进阴影贴图:正面深度对斜坡极端敏感,改渲染背面后比较基准变成物体背面深度,接收点深度几乎总小于纹素深度,粉刺大幅减少——代价是薄面片(叶片、布料)从背面看没有厚度会漏光,所以背面渲染与薄片几何需要按材质开关。
偏置解决「单张贴图内部的质量」,级联解决「分辨率的空间分配」。思路朴素:把摄像机视锥沿深度切成若干子视锥,每个子视锥独立渲染一张阴影贴图,着色时选覆盖当前片元的那一张。近处级联覆盖范围小,同样 1024×1024 的分辨率下每颗纹素摊到的世界面积小;远处级联牺牲精度换覆盖。微软技术文章直接把 CSM 称为对抗透视走样最流行、实践上最好的方案。
第一层工程决策是划分方案。数学上最优是对数划分——每颗纹素摊到的走样量对每一段视深都相等;PSSM 论文(Zhang 等,i3D 2006)给出了按视锥平行平面切分的完整方案。但实践中对数划分随近平面剧烈变化,每一帧都在动,阴影边缘随之闪烁;微软文章的经验法则是使用静态划分区间——按场景类型(贴地镜头、空中镜头、驾驶镜头)预先调好一组比例,运行时不改。这里出现了 CSM 工程的核心矛盾:理论最优解与时间稳定性的冲突,而所有 shipping 引擎的答案都一样——稳定性优先,静态区间,划分比例作为美术与工程师共同的调参项。折中方案是「实用划分」:对数与线性划分的插值,近平面稳定时偏向对数、不稳定时滑向线性,变化本身做平滑。
第二层决策是正交投影的拟合与稳定。每张级联的光源正交视锥要拟合对应子视锥在光空间的包围盒——拟合到场景包围盒会浪费分辨率,拟合到视锥才对;但微软文章同时警告:不要为了更紧而逐帧裁剪包围盒尺寸,因为「光源投影尺寸逐帧恒定」是下一层稳定性的前提。这个稳定性手段就是 texel snapping(纹素对齐):摄像机移动时,光源空间里整个场景在平移,阴影贴图的采样格点相对世界在滑动,阴影边缘每帧亮暗交替——闪烁(shimmering)。修法是把光源平移量量化到纹素尺寸的整数倍:计算子视锥中心在光空间的坐标,按纹素世界尺寸取整后再构造正交矩阵,让整张贴图以纹素为单位「步进」而不是连续滑动。这个方案自 DirectX 时代写进微软技术文章(Moving the Light in Texel-Sized Increments 一节),到今天仍是所有 CSM 实现的标准动作,伪代码不过十行:
// texel snapping:消除相机移动引起的阴影边缘闪烁
texelSize = (orthoMax.x - orthoMin.x) / shadowMapResolution;
center = transform(frustumCenterLS, lightView); // 光空间中心
center.x = floor(center.x / texelSize) * texelSize; // X 量化
center.y = floor(center.y / texelSize) * texelSize; // Y 量化
orthoMin.xy = center.xy - halfExtent.xy; // 尺寸保持逐帧恒定
orthoMax.xy = center.xy + halfExtent.xy;
// 用 orthoMin/orthoMax 重建正交投影 → 阴影边缘静止
第三层是级联间的过渡与采样。两层级联交界处分辨率突变会显示为阴影质量的台阶,常规解法在交界带做抖动混合:按片元到级联边界的距离,在两级采样结果之间做阈值抖动的插值。方向光的级联数量是另一个预算参数:二到四张是常见区间,每加一张就多一遍几何提交,与后面将讲到的页式虚拟化方案相比,CSM 的总分辨率上限就是「张数 × 单张分辨率」,这个天花板在电影级资产密度面前会先撞墙——这正是 UE5 转向虚拟化的直接动因。
虚拟阴影贴图把「分辨率天花板」问题换了一个维度解决。Epic 的官方定义很直白:概念上就是一张极高分辨率的阴影贴图,当前实现为 16k×16k 的虚拟分辨率;为了内存可行,把它切成 128×128 的页(tile),只对屏幕上实际需要着色的像素所对应的页做分配与渲染,分配依据是对深度缓冲的分析;页在帧间缓存,只有被移动物体或光源变动失效时才重绘。方向光再叠加 clipmap 结构进一步提升远距离分辨率。这套机制与虚拟纹理同源——按需分页、页池、失效重绘,只是失效语义从「换 mip」变成了「几何或光源动了」。
为什么这个方案在 UE5 时代才成为默认?因为它是为 Nanite 量身的。官方文档明确:VSM 与 Nanite 协同设计,目标是让电影级密度的几何在近景也能有致密阴影;Nanite 几何在任何距离都渲染进 VSM;反过来,Nanite 几何不支持传统 Stationary 光源的多套阴影技术,开了 Nanite 的项目要么用 VSM 要么用 Movable 光源。这是一次绑定式升级:虚拟化几何把三角形密度推到传统阴影贴图无法匹配的量级,虚拟化阴影把分辨率推到能接住的量级,两者互为存在理由。Fortnite 的技术博客把替换前后的方案堆叠讲得很清楚:过去是「近处常规级联 + 距离场阴影 + 一张远景级联 + 角色专属对象阴影 + 屏幕空间接触阴影」五套技术叠罗汉,级联之间和方案之间的过渡带肉眼可见;VSM 用单一统一路径替代了全部五套。
但虚拟化不是免费午餐,它把复杂度从分辨率转移到了缓存与失效语义上。页缓存的前提是「光源和物体不动」,而真实场景恰好相反:Fortnite 遇到的两个杀手是大规模植被动画形变和持续移动的太阳——前者让树冠对应的页每帧失效,后者让方向光的所有页每帧失效,缓存收益直接清零。他们的应对包括:与水体团队合作为单层水实现深度 prepass 来正确标记页;配合 Nanite 光栅化做更紧的集成让失效范围尽量小。文档层面同样承认页池耗尽的现实:池子不够时表现为方块状阴影与偏置异常,解法只有加内存、降分辨率或减光源数。给自研引擎的启示是:VSM 的工程重心不在算法而在失效追踪——物体级脏标记、光源级脏标记、页引用计数、分配器碎片整理,每一项都是标准虚拟内存课题在 GPU 资源上的重演。
选型上还有一个诚实的边界:官方文档直说,如果目标平台或项目不用 Nanite,VSM「很可能不是好选择」。传统 CSM 在中低密度场景下依然是实现成本、内存、带宽三维上更便宜的方案;VSM 的收益只有在几何密度高到传统方案需要天文数字分辨率时才兑现。另外官方文档把投射走样也带进了虚拟化时代:浅角度光仍然会耗尽虚拟分辨率,且「没有简单方案」——虚拟化改变了分辨率的分配方式,没有废除光学。
方向光之外,点光源与聚光灯的阴影走的是另一条路。点光源需要立方体阴影贴图:六个 90 度面各自一张深度图,工程问题从分辨率分配变成六张图的打包——常见做法是塞进同一张图集或纹理数组,采样时按面索引;近平远面策略沿用第三节的紧拟合,只是视锥变成金字塔。双抛物线阴影图是六面体的经典替代,用两张抛物线参数化覆盖整个球面,省掉四分之三的绘制但引入参数化畸变,边缘精度下降需要偏置补偿,现代引擎多在低端管线保留它作为降级路径。Godot 的文档展示了另一个工程现实:方向光的阴影图集在多个方向光之间共享,每多一个开阴影的方向光,每个光的有效阴影分辨率就被摊薄——图集预算是全局资源,不是每光独立的。
硬边阴影在视觉上永远差最后一步:真实光源有面积,半影有物理来源。PCSS(百分比渐进软阴影)按「遮挡体搜索半径估计半影大小」的思路让影子在接触点锐利、远离遮挡体后变软;代价是每像素多一轮遮挡体搜索加大核滤波,Godot 文档在推荐配置里直接给出使用纪律——同一时间只给少数关键光源开 PCSS、给用户提供关闭开关,方向光上把光角距离设为零即可退回硬阴影。UE5 在 VSM 上给出的答案是 SMRT(Shadow Map Ray Tracing):在阴影贴图页内沿光线方向步进采样,同时产出软影与接触硬化——遮挡物越靠近接触点影子越锐利。文档同时指出了它与旧方案的关系:过去基于 PCF 的方案会过度模糊,抹掉高分辨率几何带来的细节,SMRT 的定位正是接住 VSM 高分辨率红利。还有一个来自 Fortnite 的实用注脚:VSM 精度上来之后,旧的屏幕空间接触阴影不再必要,官方不推荐再叠加——层层叠叠的补偿方案在新管线到位后应当拆除而不是保留。
阴影管线的工程形态由几个全局约束决定。第一是 pass 排布与几何提交复用:所有级联和所有光源的深度 pass 合计是场景几何的 N 遍重绘,排序上按光源聚类、按材质 PSO 归并,能省的 draw call 与状态切换都以十计;有深度 prepass 的管线可以让阴影 pass 复用部分剔除结果,VSM 的页分配分析也直接消费主深度缓冲,pass 顺序在 VSM 管线里是硬依赖而不是优化项。第二是图集与数组的选择:多张小图用纹理数组比图集少掉一级 UV 管理,但图集能在一次 draw 里铺更多内容,取舍按目标硬件的纹理数组支持度定。
跨平台差异里最值得单独记一笔的是移动端。TBDR 架构(tile-based deferred rendering)GPU 上,深度 pass 与阴影采样本身都不贵,贵的是打破 tile 局部性的大图集随机采样与超出 on-chip 预算的滤波核;移动管线的常见形态是两到四张紧凑级联、硬阴影或小核 PCF、方向光图集共享(Godot 文档描述的摊薄模型在移动端更敏感),PCSS 默认关闭。本系列早前讨论过 TBDR 的 subpass 合并代价,阴影 pass 的位置选择在移动端同样遵循「尽量留在 tile 内、减少对主深度缓冲的全量回读」的原则,这里不再展开。另一个平台现实是深度格式:16 位深度在紧近平远面下可用,是省带宽的显著杠杆;反向 Z(reversed-Z)提升远处精度,与浮点深度配合是桌面标配,但要注意所有偏置系数在反向 Z 下符号与尺度都要重调——这是移植时最常见的翻车点之一。
阴影是最依赖可视化的渲染子系统。第一层可视化是把每张级联染上不同颜色叠到屏幕上:级联越界、选错级联、混合带失效一眼可见。第二层是阴影贴图本体查看器:直接以灰度显示光源视角深度,粉刺的来源(是坡度还是精度)在这一层就能分辨——全屏均匀的细纹是偏置不足,贴着几何轮廓的斑驳是精度不足。第三层是页可视化(虚拟化方案专属):把页池占用、每帧失效页数画出来,Fortnite 那种植被形变引发的失效风暴在这个视图里无所遁形;没有这一层,VSM 的性能问题几乎不可归因。
常见 bug 清单值得沉淀成 checklist:反向 Z 迁移后偏置符号未调;PCF 采样核没有对齐半纹素导致边缘抖动;级联切换的索引选择用了插值边界导致接缝闪烁;texel snapping 只做了平移量化而正交尺寸仍在逐帧变化(量化失效);立方体贴图各面的 Y 翻转不一致;法线偏置用了着色法线导致凹凸细节自阴影;失效标记漏了骨骼动画角色导致影子停在旧姿势。抓帧工具(RenderDoc 一类)里检查阴影 pass 的深度输出与最终采样是定位这类问题的最后手段,但要先有上面这层「症状到疑因」的映射,盲抓帧的收益很低。
质量指标上,可量化的三件事是:每光源每帧的页分配数与失效数(虚拟化)、阴影 pass 的几何提交量与 GPU 时间占比、以及固定相机路径下阴影边缘的时间稳定性(自动化像素差分可以量化闪烁)。把第三项放进 CI 的团队不多,但它恰好测的是玩家最敏感的那个维度。
把全文收敛成可执行的决策序列。第一问:几何密度到哪一档?电影级密度且已有虚拟化几何——直接上 VSM 路线,把工程预算砸在失效追踪与页池管理;中低密度——CSM 路线,工程预算砸在偏置与稳定性上。第二问:光源结构?单一方向光主导的户外场景是 CSM/VSM 两者各自的最优场景;大量局部光源则优先解决图集、数组和页池的共享预算问题,再谈单光源质量。第三问:目标平台?移动端锁定紧凑级联加小核滤波,不要把桌面参数直接带过去。
六条具体清单:一是偏置系统按「法线偏置为主、深度偏置为辅、背面渲染按材质开关」组织,三个参数全部随纹素世界尺寸缩放;二是级联区间静态化,texel snapping 强制正交尺寸恒定,这两条是时间稳定性的全部前提;三是近平远面用视锥与场景相交裁剪,紧到 16 位深度可用;四是虚拟化方案必须先建页可视化与失效统计,再调性能;五是软影方案按光源重要度分级供给——关键光源 PCSS/SMRT、其余硬阴影,并提供全局降档开关;六是把「阴影边缘时间稳定性」纳入自动化验证,闪烁是最贵也最容易被忽略的回归。
阴影管线的演进史其实是一个不断把「艺术问题」变成「内存与缓存问题」的过程:偏置时代比的是调参功力,级联时代比的是分辨率分配,虚拟化时代比的是失效语义的正确性。2026 年入场一个新引擎,答案大概率从 VSM 形态出发——但把 CSM 路线的每个工程节点吃透仍然值得,因为虚拟化方案失效语义的每一种 Bug,在传统方案里都有它的原型。
把阴影当管线而不是贴图来治理:深度偏置与 texel snapping 守住单张贴图的质量与时间稳定性,级联划分用静态区间分配分辨率,虚拟阴影贴图用按需分页和失效追踪接住电影级几何密度——三条主线合起来,才是现代引擎阴影管线的完整工程决策链。
Conversation
0 条