
1. 从一张“假”阴影图说起为什么三维 GIS 里阴影总是不对劲做三维 GIS 可视化的人大概率都经历过这样的场景场景里加载了几十平方公里的地形和建筑白模太阳光一打建筑该有的投影要么糊成一团要么边缘锯齿像狗啃镜头一拉远阴影直接消失拉近了又出现一条明显的分界线。更让人头疼的是倾斜摄影模型和人工建模混在一起时阴影方向甚至对不上甲方看一眼就说“这光不对”。这些问题的根源基本都指向同一个技术点Shadow Map阴影贴图。它是实时渲染里最主流的阴影方案也是游戏引擎和三维 GIS 引擎里默认开启的那一套。但“默认开启”和“调好”之间隔着一条很深的沟。Shadow Map 的原理本身不复杂可一旦放到三维 GIS 这种大范围、多尺度、异构数据混杂的场景里参数怎么配、级联怎么分、精度怎么保就变成了一门实打实的经验活。这篇内容我打算把 Shadow Map 从原理到三维 GIS 落地这条链路完整拆一遍。适合谁看如果你在做 Cesium、Unreal、Unity 或者自研三维引擎的可视化开发被阴影锯齿、阴影丢失、性能掉帧折磨过那这篇基本能对上你的痛点。如果你只是刚接触实时渲染想搞明白“阴影到底是怎么算出来的”前半部分的原理拆解也能让你少走弯路。我会尽量用从业者之间聊天的口吻把那些文档里不会写、但实际项目里一定会踩的坑讲清楚。2. Shadow Map 到底在算什么一张深度图背后的取舍逻辑2.1 核心思路从光源视角“拍一张照片”Shadow Map 的思路其实特别朴素用一句话概括就是站在光源的位置往场景里看把每个像素离光源最近的深度记下来然后再从摄像机视角去比对比这个深度远的就说明被挡住了也就是在阴影里。你可以把它想象成光源拿了一台深度相机对着场景拍了一张“深度照片”。这张照片不记录颜色只记录每个位置离光源有多远。渲染正式画面时对于屏幕上的每个点把它换算到光源的视角下看看它离光源的距离是不是比照片里记录的最近距离更远。如果是说明这个点和光源之间还隔着别的东西它就在阴影里。这个逻辑决定了 Shadow Map 的几个天然特性。第一它是基于图像空间的精度受限于那张深度图的分辨率所以必然会有锯齿。第二它记录的是离散的深度采样所以会有自遮挡导致的“阴影痤疮”shadow acne。第三它只关心光源能看到的部分光源看不到的地方比如背光面本来就在暗处不需要额外处理。理解了这三点后面所有的参数调整其实都是在跟这三个特性做博弈。2.2 为什么不用光线追踪实时场景的算力账有人会问既然 Shadow Map 这么多毛病为什么不直接上光线追踪从每个像素往光源打一条射线有没有遮挡一测便知物理上完全正确。答案就一个字贵。光线追踪的阴影计算量跟场景三角形数量、光源数量、像素数量都强相关在三维 GIS 这种动辄上亿三角面、几十平方公里范围的场景里实时跑光线追踪阴影基本不现实。即便现在有硬件加速GIS 场景的几何复杂度也远超一般游戏关卡。Shadow Map 的聪明之处在于它把“每个像素测一次遮挡”这个 O(像素数 × 几何数) 的问题转化成了“渲染一张深度图 每个像素做一次深度比较”。深度图的渲染是一次性的跟屏幕分辨率无关只跟阴影图分辨率有关深度比较是纯数值运算几乎不耗算力。这就是它能成为实时阴影主流的根本原因。注意Shadow Map 的“便宜”是相对的。阴影图分辨率开太高、级联层数太多、光源数量太多照样能把 GPU 吃满。三维 GIS 里经常要同时处理太阳光、环境光、补光阴影预算必须精打细算。2.3 自遮挡与阴影偏移那个绕不开的 bias 参数Shadow Map 最经典的副作用就是阴影痤疮。原因是深度图是离散采样的一个像素记录的深度值代表的是它覆盖的那一小块区域。当摄像机视角下的某个点换算到光源视角时它落在两个采样点之间比对深度时就会因为精度误差误判成“被遮挡”于是本该受光的表面出现一条条黑色条纹。解决办法是加一个depth bias也就是在比对深度时给一个偏移量让判定稍微“宽容”一点。但 bias 加多了会出另一个问题peter-panning阴影跟物体“脱开”看起来像飘在空中。这两个问题是一对矛盾bias 小了长痤疮bias 大了阴影飘。实际调参时bias 的取值跟阴影图分辨率、场景尺度、光源角度都有关。我的经验是先用一个较小的固定 bias 打底再配合normal offset沿法线方向偏移采样点来压制痤疮这样比单纯加大 depth bias 效果更干净也不容易飘。问题现象成因常用对策副作用阴影痤疮条纹深度采样精度不足增大 depth bias、normal offsetbias 过大导致阴影飘Peter-panningbias 过大减小 bias、改用 normal offset痤疮可能复现阴影边缘锯齿阴影图分辨率不足提高分辨率、PCF 滤波显存和带宽上升阴影断裂/丢失级联切换或范围不足调整级联划分、扩大覆盖精度下降3. 级联阴影三维 GIS 大场景的必选项3.1 为什么单张阴影图在大场景里必然失效三维 GIS 的典型场景是“大范围 近处细节”。比如一个城市级场景视野可能覆盖十几公里但用户又需要看清近处一栋楼的窗户投影。如果只用一张阴影图覆盖整个视野那么这张图上的每个像素要负责的地面面积就非常大。假设阴影图是 2048×2048覆盖 10 公里范围那每个像素对应地面约 5 米。5 米的精度近处建筑的阴影边缘会糊成什么样可想而知。反过来如果为了近处精度把阴影图覆盖范围缩小到 500 米那远处就完全没有阴影了镜头一拉远阴影“啪”地消失视觉上非常突兀。这就是**级联阴影贴图Cascaded Shadow MapCSM**要解决的问题。它的思路是把摄像机的视锥体按距离切成几段每段用一张独立的阴影图来渲染近处的那张覆盖范围小、精度高远处的那张覆盖范围大、精度低。这样既保证了近处细节又兼顾了远处的大范围阴影。3.2 级联怎么分对数划分与实用混合级联划分是 CSM 里最需要经验的一环。划分方式主要有两种均匀划分和对数划分。均匀划分就是按距离等分简单直接但不符合人眼对近处细节更敏感的规律。对数划分则让近处的级联更密集远处的更稀疏更符合视觉需求。实际工程里用得最多的是两者的混合用一个 lambda 参数控制对数划分的权重通常取 0.5 到 0.7 之间。在三维 GIS 里我的习惯是根据场景尺度来定级联数量和划分。一般 3 到 4 级就够用级联太多会导致切换时的接缝问题变严重而且每级都要渲染一遍深度图性能开销线性增长。具体划分时第一级通常覆盖摄像机前方 50 到 100 米保证近处建筑和地物的阴影质量第二级覆盖到 300 到 500 米第三级覆盖到 1 到 2 公里第四级可以覆盖到 5 公里以上主要给大范围地形起伏提供方向性阴影。这个数值不是固定的要结合你的场景尺度和相机高度来调。实操心得级联划分的边界最好避开场景里阴影特别密集的区域。如果一条级联分界线正好切过一片建筑群切换时会出现明显的阴影跳变。我一般会观察场景里建筑最密集的地带把分界线挪到相对空旷的位置。3.3 级联切换的接缝问题与淡化处理级联之间切换时因为两张阴影图的分辨率和覆盖范围不同同一个物体在两级交界处可能出现阴影深浅不一致形成一条可见的接缝。这个问题在三维 GIS 里尤其明显因为地形是连续的接缝会沿着一条线横穿整个画面。常见的处理方式是级联淡化cascade blend在级联边界附近的一个过渡带内同时采样两张阴影图按距离做插值混合。这样接缝就被“抹平”了。过渡带的宽度一般取级联覆盖范围的 5% 到 10%太窄起不到淡化作用太宽会让过渡区域变模糊。另一个容易被忽略的点是级联的稳定性。当摄像机移动时如果级联的划分跟着相机位置实时变化阴影边缘会出现“爬行”或“抖动”。解决办法是把级联的投影矩阵做纹素对齐让阴影图的采样网格跟世界空间对齐这样相机移动时阴影不会闪烁。这个细节在游戏引擎里通常已经内置处理但自研引擎或者深度定制时一定要自己实现。4. 三维 GIS 里的 Shadow Map 落地从参数到性能的完整链路4.1 场景尺度与阴影图分辨率的匹配计算三维 GIS 场景的尺度跨度极大从几米的构件到几十公里的地形都有。阴影图分辨率不能拍脑袋定得根据场景实际需要来算。一个实用的估算方法是先确定你希望近处阴影的最小可分辨细节比如 0.5 米。然后看第一级级联覆盖多大范围比如 100 米。那么第一级阴影图的分辨率至少要是 100 / 0.5 200 像素。但这是理论下限实际因为要留滤波余量和视角倾斜通常要乘以 2 到 4 倍也就是 512 到 1024。再考虑级联数量如果 4 级都用 1024总显存占用是 4 张 1024×1024 的深度图对现代 GPU 来说完全可以接受。但如果场景里有大量细小构件比如栏杆、窗框0.5 米的精度就不够了可能需要 0.1 米甚至更高。这时候要么提高第一级分辨率到 2048要么缩小第一级覆盖范围。我的建议是优先缩小覆盖范围因为分辨率翻倍带来的显存和带宽开销是平方级的而缩小范围只是线性影响。场景类型第一级覆盖建议分辨率说明城市级 GIS80-120 米2048建筑细节多需高精度地形为主150-200 米1024-2048地形起伏对精度要求稍低园区级50-80 米1024范围小精度容易保证大范围地形200-300 米2048需兼顾远处级联数可增加4.2 深度图格式与精度选择Shadow Map 的深度图格式直接影响精度和性能。常见的有D16、D24、D32几种。D16 只有 16 位深度精度有限在大范围场景里容易出现深度冲突一般不推荐。D24 是 24 位深度配合 8 位模板缓冲是大多数引擎的默认选择精度和性能平衡得比较好。D32 精度最高但显存和带宽开销也最大通常只在极端场景下使用。在三维 GIS 里因为场景深度范围可能从几米到几十公里深度值的分布非常不均匀。如果直接用线性深度近处的精度会被远处的范围“稀释”掉。所以实际实现里通常用非线性深度或者把光源的投影矩阵做深度范围裁剪只保留真正需要产生阴影的那一段深度。这个裁剪范围要根据场景里最高和最低的阴影投射物来定裁得太紧会导致部分物体阴影丢失裁得太松又浪费精度。注意深度范围裁剪要留一定余量因为地形起伏和建筑高度可能超出预期。我一般会在实际场景包围盒的基础上上下各留 10% 到 20% 的余量。4.3 滤波方案PCF、PCSS 与 VSM 的取舍Shadow Map 的锯齿问题靠滤波来缓解。最基础的是PCFPercentage Closer Filtering在比对深度时不是只采样一个点而是采样周围一圈点把结果平均。PCF 的核越大阴影边缘越柔和但采样次数也越多。3×3 的 PCF 是 9 次采样5×5 是 25 次性能开销增长很快。PCSSPercentage Closer Soft Shadows更进一步它根据遮挡物和接收面的距离动态调整滤波核大小实现“近处阴影硬、远处阴影软”的效果更符合物理规律。但 PCSS 需要额外计算遮挡物平均深度开销比 PCF 大不少在三维 GIS 这种大场景里要谨慎使用。VSMVariance Shadow Maps走的是另一条路它不存深度而是存深度的均值和平方均值通过统计方法估计遮挡概率。VSM 的好处是滤波非常快可以做很大的滤波核阴影边缘很柔和。但它的缺点是漏光light bleeding在遮挡物和接收面距离很近时容易出现不该有的亮斑。三维 GIS 里建筑和地面贴合紧密漏光问题会比较明显所以 VSM 用得相对少。我的实际选择是默认用 PCF核大小 3×3 到 5×5配合级联和合适的 bias。如果项目对阴影柔和度要求高再考虑 PCSS。VSM 除非有特殊需求一般不上。4.4 性能优化把算力花在刀刃上Shadow Map 的性能开销主要来自三块深度图渲染、深度比对、滤波采样。优化也要从这三块入手。深度图渲染方面剔除是最有效的手段。光源视角下看不到的物体不需要渲染进阴影图视锥剔除和遮挡剔除都要开。另外LOD也很关键远处的物体用低模渲染阴影近处用高模能省下大量三角形。三维 GIS 里倾斜摄影模型面数很高如果全部参与阴影渲染性能会崩通常只让白模和主要地物投射阴影倾斜摄影本身不投射或只用简化版本投射。深度比对和滤波方面减少采样次数是核心。PCF 核不要盲目开大3×3 在大多数情况下够用。另外可以把阴影计算放到半分辨率的缓冲里再上采样回全分辨率这样深度比对和滤波的开销直接减半代价是阴影边缘稍微模糊一点但在三维 GIS 里通常可以接受。还有一个容易被忽略的点是阴影更新频率。如果场景里只有相机在动光源和物体都不动阴影图其实不需要每帧重渲染。可以隔帧更新或者只在光源或物体变化时更新。这个优化在静态场景里能省下一大半开销。5. 踩坑实录三维 GIS 阴影调试的常见问题与排查5.1 阴影突然消失或闪烁这是三维 GIS 里最高频的问题。表现是镜头拉到某个距离阴影突然没了或者边缘疯狂闪烁。原因通常有三个。第一是级联覆盖范围不够相机拉远后物体超出了最后一级级联的覆盖范围自然就没有阴影了。解决办法是扩大最后一级的覆盖或者增加级联数量。第二是深度范围裁剪过紧物体超出了光源投影矩阵的深度范围被裁掉了。这时候要检查光源的正交投影矩阵参数把 near 和 far 调够。第三是纹素对齐没做好相机移动时阴影图采样网格跟着抖导致边缘闪烁。这个要检查级联投影矩阵的纹素对齐逻辑。排查顺序我一般是先看级联覆盖范围再看深度裁剪最后查纹素对齐。大部分情况是前两个。5.2 阴影边缘锯齿严重锯齿严重说明阴影图分辨率相对覆盖范围不够。先算一下当前每个纹素对应多少世界空间距离如果超过你需要的精度就得调整。调整方向有两个提高分辨率或者缩小覆盖范围。前面说过优先缩小覆盖范围。如果第一级覆盖 100 米、分辨率 1024每个纹素约 0.1 米这个精度对大多数建筑阴影够用了。如果还不够再考虑提到 2048。另外PCF 滤波也能在一定程度上柔化锯齿但它是“模糊”不是“消除”根本问题还是分辨率。5.3 阴影痤疮反复出现痤疮问题前面讲过但实际调试时经常反复。有时候调好了换个场景又冒出来。这是因为 bias 的最优值跟场景尺度、光源角度都有关不是一个固定值能通吃的。我的做法是用 normal offset 为主depth bias 为辅。normal offset 沿法线方向偏移采样点对斜面特别有效而且不容易导致 peter-panning。depth bias 只用来兜底处理极端情况。另外斜率缩放 bias也很有用它根据表面法线和光源方向的夹角动态调整 bias斜面上给大一点正面上给小一点比固定 bias 智能得多。5.4 性能掉帧定位阴影导致的掉帧定位起来其实有套路。先用 GPU 性能分析工具看阴影相关的 draw call 和渲染时间占比。如果深度图渲染占大头说明是几何量太大要上剔除和 LOD。如果深度比对和滤波占大头说明是采样太多或分辨率太高要降核大小或降分辨率。还有一个隐蔽的坑是阴影图格式。有些平台对 D32 的支持不好会走软件路径性能直接崩。如果发现阴影开销异常高先检查深度图格式是不是用了不支持的。问题可能原因排查动作解决方向阴影消失级联覆盖不足检查最后一级范围扩大覆盖或加级联阴影闪烁纹素未对齐检查投影矩阵对齐实现纹素对齐边缘锯齿分辨率不足算纹素世界尺寸提分辨率或缩范围阴影痤疮bias 不足检查斜面和正面normal offset 斜率 bias性能掉帧几何或采样过多GPU 分析工具剔除、LOD、降采样5.5 多光源阴影的取舍三维 GIS 里有时不止一个光源比如太阳光加补光。如果每个光源都开 Shadow Map开销直接翻倍。实际项目里通常只让**主光源太阳**投射阴影补光不投射或者只用很低的精度投射。人眼对主阴影敏感对补光阴影不敏感这个取舍很划算。如果确实需要多光源阴影可以考虑阴影图集shadow atlas把多个光源的阴影图打包到一张大图里减少纹理切换开销。但实现复杂度会上升要权衡。6. 写在最后一些不写在文档里的经验Shadow Map 这个东西原理看一遍就懂但调好真的需要时间。我在三维 GIS 项目里摸爬滚打这几年最大的体会是不要追求物理正确要追求视觉合理。GIS 场景不是游戏关卡不需要每个像素都精确用户看的是整体效果和流畅度。级联划分、bias、滤波核这些参数没有一套放之四海皆准的值。我的习惯是先在典型视角下把近处调好再拉远看远处反复几次找到平衡点。另外多准备几套预设城市级、园区级、地形级各一套切换场景时直接套用比每次重新调省事得多。还有一个小心得阴影颜色不要用纯黑稍微带一点环境光的色调看起来会自然很多。纯黑阴影在 GIS 场景里会显得很“假”带一点蓝紫或暖灰跟天空和环境光呼应整体质感会提升一个档次。这个不是技术问题是审美问题但往往就是这些细节决定了最终效果能不能让甲方点头。