阴影贴图
Shadow Mapping
阴影贴图 (Williams, 1978) 先从光源位置渲染深度,再从相机渲染时把每个点到光源的距离与贴图中存的深度比较:更远就在阴影中。它开销可预测(生成约与图元数成线性、查询为常数时间),非常适合 GPU,是任意表面阴影最常用的算法。主要问题是分辨率、深度精度带来的混叠、阴影痤疮和漏光。
基本做法
- 第一遍只需 z-buffer,关闭光照、纹理和颜色写入;存下的深度即阴影贴图(也叫阴影深度图、阴影缓冲)。第二遍从相机渲染,用纹理映射比较。
- 光源和物体都不动时(如 CAD)可生成一次每帧复用。
- 方向光(太阳)用正交投影,宽高要覆盖所有向视景体投影的物体;聚光灯有天然视锥;被遮挡体包围的局部点光源用六面立方体,称为全向阴影贴图,难点在两张图接缝处的伪影。Crytek 根据每个面视锥在屏幕上的覆盖决定其分辨率,全部存入图集。
缩小光源视锥
- 只渲染能投影的物体(只接收阴影的地面不必渲染)。
- 根据可见接收体收紧光源视锥:超出可见接收体最远距离的物体不可能投影到它们上;x、y 范围也可缩到可见接收体。这既省渲染,又让阴影贴图有效分辨率更高。近平面尽量远、远平面尽量近,以提高深度精度。
阴影痤疮与偏移
- 自阴影混叠(surface acne / shadow acne):三角形被错误判定为遮挡自己。原因一是数值精度,二是几何上用一个点样本代表一块区域的深度,光源样本位置与屏幕样本几乎从不重合。
- 常数偏移:比较时从接收体距离中减去一个小偏移;表面不正对光源时会失效。
- 斜率缩放偏移:偏移与表面相对光源的倾角成正比,可用
glPolygonOffset之类的命令实现。正对光源时斜率偏移为 0,所以要和常数偏移一起用;接近掠射时正切值极大,通常夹取上限。 - 法线偏移 (Holbert):把接收体世界位置沿法线移动,与光方向和几何法线夹角的正弦成正比,同时改变比较的深度和贴图坐标,相当于移到表面上方的"虚拟表面"。Pettineo 建议按阴影贴图深度范围缩放;也可沿相机视线方向偏移。
- 偏移过大导致漏光 / Peter Panning:物体像浮在表面上,例如脚下地面不再有阴影。
- 第二深度阴影贴图:只把背面画入阴影贴图,适合不能手调偏移的系统;双面薄物体(棕榈叶、纸)、轮廓附近、物体接触处仍有问题,且更容易漏光。Sousa 等发现太阳用正面、室内光用背面效果最好。
- 物体必须"水密"(流形且封闭),否则要正反面都画入,才能完整投影。双阴影贴图:记录离光源最近的两个表面,取其中点深度,物体足够厚时痤疮和漏光都很少,但要两张阴影贴图的开销。
稳定性
光源视体随相机变化会导致阴影逐帧轻微抖动。方向光的解决方法:强制每张阴影贴图在世界空间保持相同的纹素"光束"位置,相当于给整个世界施加一个固定的二维网格,光源投影被对齐到这个网格。
分辨率问题
- 理想情况下一个阴影纹素约覆盖一个屏幕像素。近处许多像素对应同一纹素称为透视混叠;表面几乎平行于光线但正对观察者时称为投影混叠。提高分辨率可以缓解,但耗内存和处理时间。
- 透视变形方法(PSM、TSM、LiSPSM):通过改变光源视角方向和投影窗口,让近处获得更高采样率,只需修改光源矩阵。光照方向与视线垂直(如头顶光)时效果最好;但光源在相机前方并对着相机("对决视锥"、"车灯前的鹿")时线性变形只会更糟,加上质量突变、相机移动时阴影"神经质"不稳定,这类方法已不再受青睐。
- 多张阴影贴图的思路由此发展为级联阴影贴图。
不规则 z-buffer (IZB)
- 阴影贴图的离散采样导致相机看到的位置与光源采样位置不一一对应,产生混叠和偏移问题。IZB 反过来:先从相机渲染得到可见点,把它们变换到光源视图并存入对应纹素的列表;再对遮挡体做保守光栅化,在像素着色器中测试每个点是否被三角形挡住(相当于从点向光源射线追踪,也称视锥追踪)。
- 没有采样和偏移问题,得到完美锐利的阴影。实现要仔细:Wyman 等的最终版本比原型快两个数量级(剔除背光点、不为空纹素生成片元、让每个纹素列表短而长度相近以减少线程分歧);配合级联可缩短近处的列表。
- 《The Division》按遮挡体距离混合 IZB 硬阴影与 PCSS 软阴影,近处用硬阴影,且只对选定的物体子集使用 IZB 以降低开销。
其他应用
- 地形遮挡高度(CD Projekt):为太阳渲染阴影贴图后,对每个地形位置沿竖直方向固定步长上升直到看见太阳,再二分搜索,得到"仍被遮挡的最大高度",插值后既能给地形也能给树木等物体投影(《巫师》)。
- 屏幕空间阴影:阴影贴图分辨率有限,难以表现面部等小特征(比如不该发光的鼻孔)。把相机深度缓冲当作高度场,向光源方向光线步进检查遮挡;开销大,但在过场特写中值得,已被许多引擎采用。
- 半透明物体可再生成一张存最近深度和颜色/覆盖率的阴影贴图,用于给其后的接收体染色(不能用于半透明物体自身)。头发、烟雾等需要深度阴影贴图(每纹素存随深度变化的透光函数)、不透明度阴影贴图或自适应体积阴影贴图(像素着色器中在线有损压缩若干个不透明度/深度对,需要 UAV 和 ROV;《GRID2》中烟雾平均开销低于 2 ms/帧)。
- 稀疏纹理可用于阴影贴图存储以提高分辨率;也有把阴影转为稀疏体素八叉树、用体素锥追踪生成面光源软阴影的方法。
总结(书中结论)
| 技术 | 用途 |
|---|---|
| 阴影贴图 | 任意表面阴影的最常用方法 |
| 级联阴影贴图 + SDSM | 大范围(户外)采样质量与精度 |
| PCF | 一些柔化 |
| PCSS 及变体 | 接触硬化 |
| IZB | 精确硬阴影 |
| 滤波阴影贴图 | 快速软阴影,遮挡体远离接收体时(如地形)尤佳 |
| 屏幕空间阴影 | 额外精度,代价明显 |
GEA 的视角:阴影贴图(卷 II 12.5.6)
- 阴影贴图本质上是从光源视角而不是相机视角做的逐片元深度测试。分两步:先从光源视角渲染场景并保存深度缓冲,得到阴影贴图;再正常渲染场景,用它判断每个片元是否在阴影中(卷 II 12.5.6)。
- 阴影贴图只存深度,所以通常用硬件的双倍速纯 z 模式渲染。点光源用透视投影,方向光用正交投影。注意单个透视视锥覆盖不了点光源的全方向(视场角须小于 180°),全向点光源实际要渲染 6 个面的立方体阴影贴图(或双抛物面贴图),GEA 的描述只适用于聚光灯或只朝一侧投影的情况(见本页上文 RTR4 对全向光源的讨论)。
- 正常渲染时,为每个顶点算出光空间坐标(生成阴影贴图时的那个"观察空间"),在三角形上插值得到片元的光空间位置;把光空间 转成阴影贴图的 ,比较片元的光空间 与该纹素存的深度:更远就说明被更近的几何挡住,在阴影中。
- 这与本页「基本做法」一致。GEA 没有讨论阴影痤疮与偏移、级联、稳定性等实践问题,这些见本页上文与 级联阴影贴图、软阴影 PCF 与 PCSS。
LearnOpenGL 的视角:深度贴图两遍流程与 acne / peter panning / PCF(Advanced Lighting / Shadow Mapping)
来源:Shadow Mapping;摘录 Advanced Lighting — Shadow Mapping。与上文 RTR/GEA 对照;此处补 OpenGL 落地。
两遍
- 从光源视角渲染,只写深度:FBO 附
GL_DEPTH_COMPONENT纹理,glDrawBuffer/glReadBuffer(GL_NONE);方向光用正交投影 +lookAt得到lightSpaceMatrix;简单 VS 做lightSpace * model * pos,空 FS 即可。 - 正常渲染:顶点输出
FragPosLightSpace;片元里透视除法 → NDC→[0,1] → 采closestDepth,与currentDepth=proj.z比较。环境项通常不乘阴影,漫反射/镜面乘(1-shadow)。
伪影与修复
- Shadow acne:多片元采同一倾斜 depth texel。用 bias,可按
max(0.05*(1-dot(N,L)), 0.005)随夹角变。 - Peter panning:bias 过大阴影与物体脱开。深度 pass 对实心物体可
glCullFace(GL_FRONT)(用法线背面深度);单面地板不适用。 - 视锥外过采样:
CLAMP_TO_BORDER边框深度 1.0;proj.z>1强制非阴影。 - PCF:邻域多点比较再平均(教程 3×3);
textureSize得 texel。更高级软阴影见 软阴影 PCF 与 PCSS。
方向光→正交;聚光/点光→透视。透视深度可视化需先线性化,比较本身用相对深度即可。
LearnOpenGL 的视角:全向阴影与深度立方体贴图(Advanced Lighting / Point Shadows)
点光需六面深度。做法:GL_TEXTURE_CUBE_MAP 六张 DEPTH_COMPONENT;glFramebufferTexture 一次挂整立方体。六份 perspective(90°) * lookAt(±X/±Y/±Z)。几何着色器用 gl_Layer 把每个三角形翻成 6 个,单 pass 填满立方体(性能未必优于六次 draw,需 profiling)。
深度存为线性距离/far_plane(片元写 gl_FragDepth)。光照 pass:fragToLight = fragPos-lightPos 当方向采 samplerCube;closestDepth*=far;currentDepth=length(fragToLight);再加 bias。
立方体 PCF:可在方向上加 3D 偏移(样例多、重),或用约 20 个大致分离的偏移方向 × diskRadius;半径可随视距变软。链 软阴影 PCF 与 PCSS、曲面细分与几何着色器(gl_Layer)。
GPU Gems 的视角:全向阴影贴图(GPU Gems 1 第 12 章)
来源:Philipp S. Gerasimov《Omnidirectional Shadow Mapping》;摘录 Chapter 12. Omnidirectional Shadow Mapping — Philipp S. Gerasimov。
点光源用立方体贴图(或双抛物面两遍)存深度;相对模板阴影体更吃动画/细分网格,可做滤波软边。双抛物面省 pass、立方体质量更稳。Gems2 STALKER 实践里常把全向拆成六盏聚光以便过滤打包。
GPU Gems 的视角:透视阴影贴图的养护(GPU Gems 1 第 14 章)
来源:Simon Kozlov《Perspective Shadow Maps: Care and Feeding》;摘录 Chapter 14. Perspective Shadow Maps: Care and Feeding — Simon Kozlov。
在后投影空间做阴影图,让近处物体分到更多 texel,对抗大场景锯齿。原文三坑:灯在相机后方时的「虚拟相机」损害质量;质量强烈依赖灯光在相机空间的位置;texel 面积非均匀导致 bias 不能再是常数。本章给方向光为主的实用修补与过滤建议,是通向 LiSPSM / 级联阴影贴图(Gems3 PSSM)的历史一环。
出处
- realtime_rendering_4ed.pdf 第 7 章 Shadows
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 12 章 12.5.6 节
- Advanced Lighting — Shadow Mapping
- learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
- Advanced Lighting — Point Shadows
- learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
- Chapter 12. Omnidirectional Shadow Mapping — Philipp S. Gerasimov
- Chapter 14. Perspective Shadow Maps: Care and Feeding — Simon Kozlov
- developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-12-omnidirectional-shadow-mapping
- developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-14-perspective-shadow-maps-care-and-feeding