4.2 阴影:平面投影 vs 阴影贴图 本节摘要:平面投影把网格沿光方向压到一张平面上,再画成深色透明,CPU 几乎只改一个矩阵,GPU 多画一次几何。它快,但只能贴地、不能自阴影、交叉会穿帮。阴影贴图先从灯的视角把深度画进离屏纹理,再在主视角采样比较,能自阴影、能上墙,但要 FBO、偏移、分辨率与过滤。选型看阴影是“有一块暗”还是“要合理遮挡”。 本节地图 阅读完本节,你应当能够: 用矩阵语言说明平面投影阴影在做什么 按步骤列出阴影贴图的离屏 pass 与主 pass 对照 acne、Peter Panning、分辨率不足三类瑕疵的成因 说明方向光用正交投影、聚光用透视投影的原因 给出电商地面影与游戏角色影的默认路线 假影是几何问题,贴图影是深度比较问题
本节摘要:平面投影把网格沿光方向压到一张平面上,再画成深色透明,CPU 几乎只改一个矩阵,GPU 多画一次几何。它快,但只能贴地、不能自阴影、交叉会穿帮。阴影贴图先从灯的视角把深度画进离屏纹理,再在主视角采样比较,能自阴影、能上墙,但要 FBO、偏移、分辨率与过滤。选型看阴影是“有一块暗”还是“要合理遮挡”。
阅读完本节,你应当能够:
平面投影:构造一个把顶点沿光方向映射到地面平面的 4x4,用它代替模型矩阵再画一遍网格,颜色纯黑、开混合、深度写入关。CPU 算这个矩阵,GPU 当普通网格画。没有第二份深度附件,没有比较。物体离开地面、地面不平、要投到墙上,合同立刻破裂。
阴影贴图:灯是一台相机。把它的 VP 拿来,把场景深度画到纹理。主相机绘制时,把世界位置变到灯的裁剪空间,取出深度纹理里的值,比一比“灯看这边谁更近”。更近的是遮挡者,当前片元在影里。这是第 3.4 节 FBO 加第 3.3 节深度再加一次采样。
平面: 同一几何 × 压扁矩阵 → 当贴花画在地上 贴图: 灯相机深度 FBO → 主相机 FS 比较
| 维度 | 平面投影 | 阴影贴图 |
|---|---|---|
| CPU | 一个投影矩阵 | 灯相机、FBO 绑定、可能级联选择 |
| GPU | 多画一次网格 | 整场景深度 pass + 主 pass 采样 |
| 接收表面 | 通常一张平面 | 任意写了比较代码的表面 |
| 自阴影 | 无 | 有 |
| 点光全向 | 很假 | 要立方体深度,更贵 |
| 实现量 | 一下午 | 要处理偏移与过滤 |
acne:深度精度与离散纹素让表面自己挡住自己,斑点。偏移(bias)或斜率缩放。偏大了,影与物体脱开,Peter Panning。没有万能 bias,按场景调,或用 PCF 多采几个深度平滑边缘。
// 概念:主 pass 里比较 // 已把位置变到灯的 NDC,uv 与 depthFromLight 在手 float closest = texture2D(uShadow, uv).r; float shadow = (depthFromLight > closest + bias) ? 0.3 : 1.0;
比较发生在 FS,所以阴影贴图天然是逐片元效果。分辨率不够时边缘呈锯齿块。级联阴影把视锥分成几段,近处用更密的贴图,CPU 要选 cascade、GPU 要采对应层。移动端常常一盏方向光一张 1024 贴图打住。

不是所有网格都要投阴影。粒子、玻璃、已经很暗的物体可剔除出深度 pass,减 GPU。接收者才采样。地面一定收;天空不收。CPU 维护投射者列表,等于又一次合批问题:深度 pass 尽量少切程序,可用一份只出深度的 VS。
多灯阴影贴图:每盏灯一张或一套 cascade,显存与 pass 线性涨。实时产品里常常“主方向光有影,点光无影或用廉价圆片”。对照要写进需求,避免程序与美术各自假设。
WebGL1 深度纹理靠扩展;无扩展时有人把深度编码进 RGBA 颜色再解码,精度与过滤更闹心。能上 WebGL2 就用深度纹理。又一次版本决定效果路线。
⚠️ 常见坑:阴影 pass 忘了把 viewport 设成贴图尺寸,或主 pass 比较时忘了把 NDC 的 xy 从负一到一映到零到一。比较全失败,全黑或全亮。先可视化深度纹理。
💡 关键直觉:平面影是“再画一个扁物体”;贴图影是“问灯的深度仓库”。一个加几何,一个加离屏。
软影:PCF 在 FS 多采;接触硬化更贵。移动端 3x3 PCF 常常是上限。不要把论文里的百分比柔和阴影直接搬进商品页。电商地面一块椭圆暗斑(有时甚至是贴图,不是投影)可能比 cascade 更合适——那是第三条更假但更稳的路:阴影当贴花纹理,连矩阵压扁都省。
能当交付起点,不能当理解终点。一键背后仍是灯相机、FBO、bias。出 acne 时你要调的是这些,不是材质颜色。把引擎阴影当固定管线开关,会回到第 1.2 节的错误记忆。
第一个实验:平面投影一个立方体影。第二个:512 深度贴图,可视化它,再比较。两步都通,再谈 PCF。跳步的人会在全黑场景里同时怀疑光照和阴影。
方向光看向整个视锥时,一张贴图近处稀疏、远处浪费。级联把视锥按深度切开,近 cascade 覆盖小范围高分辨率。CPU 算每个 cascade 的正交投影,GPU 在 FS 选层。层间接缝要混合。移动端两层常常是上限;桌面三层。层数是产品档位,不是技术荣耀。电商查看器一盏灯一张 1024、物体几乎在原点,级联是多余 CPU。
PCF 在阴影边缘多采几个深度做平均,软且贵。采样次数 4 到 9 在手机上就能感到填充。泊松盘或硬件 shadow sampler 看扩展与 2.0 能力。没有硬件比较采样时,自己写 texture2D 再比较,过滤模式必须 NEAREST,否则深度被线性混合成无意义值。这是 2.6 的“查的表用 NEAREST”。用 LINEAR 采深度,影会漏光,像 bias 调不对。先改过滤,再调 bias。
接触阴影、屏幕空间阴影、胶囊假影,都是更假或更局部的路。角色脚下一块模糊圆片,CPU 按脚与地的距离改透明,常常比贴图更稳,也没有 acne。过场动画里角色不离地,圆片足够。只有需要角色把自己的胳膊投到身上、或树影投到墙上时,才上贴图。把需求写成“自阴影:要/不要”“接收表面:仅地/任意”,路线就锁了。需求不写,程序会按论文做,运营会按圆片验收,两边都怒。
阴影比较应在线性空间乘在漫反射上,不要直接乘最终伽马色。调试时把 shadow 因子输出成灰度全屏,确认轮廓是否贴合。贴合差先查灯 VP 与 bias,再查法线偏移。法线偏移让acne 在斜面改善,过了会裂缝。参数要给美术或程序一个滑条,写进档位,不要硬编码魔法数散落着色器。魔法数是阴影模块的技术债利息。
比较方向反了,或 NDC 没映到 0 到 1,或 bias 过大过小,或深度纹理没清。可视化深度。全黑全亮都先看灯相机是否对着场景。灯 VP 抄主相机是常见错。灯是另一台相机。
bias 太大或级联选错层。减小 bias,看法线偏移。平面投影若地面不是那张平面,也会分家。分家时先确认走的是哪条路线。两条都开会更乱。
分辨率不足。加尺寸或 PCF。PCF 在手机填充分。档位:低档无树影或圆片,中档 512,高档 1024 加 4 次 PCF。不要在低档开 2048 三层级联。锯齿是离散深度,不是再乘暗色能修。
网格带是否投射、是否接收标记。加载进两个列表。深度 pass 只画投射者,主 pass 只接收者采样。列表空则整份阴影 pass 跳过。跳过比画空更重要。尺寸:低档关或圆片,中档 512,高档 1024,级联仅桌面高档。bias 滑条给程序,默认写进场景。可视化深度绑快捷键。没有可视化不许调 bias。双路线禁止同时开。需求文档写自阴影要或不要。不要写成“尽量真实”。真实没有上限,账单有。
阴影验收:立方体投在平面上应贴脚;自阴影在凹槽里应看见;bias 滑条能从 acne 调到 peter pan 再调回。滑不到中间值,说明分辨率或灯 VP 错,不是滑条不够细。投射者列表为空则跳过 pass,测试要覆盖空列表,防止空场景仍清一张 2048。双路线互斥写成断言。需求字段自阴影要或不要,加载时选路线。树叶用圆片或不要影,避免 PCF 把填充率打满。可视化深度键与 bias 滑条是本节工具,没有工具的阴影代码不准合入。合入后的第一周用档位机看发烫,发烫先降尺寸,不先加 cascade。cascade 是桌面高档的事。
阴影代码合入必须带可视化键与空列表测试。空列表仍创建 2048 附件的补丁直接打回。附件账单在没影的场景里照样付。用户看不见影,却在付 GPU 清屏。这是最冤枉的税。跳过 pass 是对照思维:没有投射者就没有这条路线。路线可以不走,不能不计费地空转。
现场对照时只改一个变量。同时改版本、封装、通道,归因会消失。消失之后只能靠感觉,感觉正是本书要拆掉的东西。把改动写成一条,测完再写下一条。一条一条,四组对照才用得上。用不上的对照表只是装饰。装饰填不满格子,格子空着就不要标完工。阴影这一格改完,用立方体贴地验收,不要直接上树林。
下一节对照纹理进阶:从一张反照率,到立方体环境,再到 PBR 那一组贴图。