本节摘要:实时阴影的主流答案是阴影映射——从光源视角渲染一遍深度存进纹理,正式渲染时把每个片元换算到光源视角比对深度判断遮挡。本节解剖这套两遍渲染的完整机制与坐标换算的每个坑,重点病理两例:阴影痤疮(深度精度抖动)与彼得潘移位(偏移补偿失衡),并给出纹理过滤扩展与点光源立方体阴影两个升级变式。这是 FBO 闭环(3.3 节)最经典的应用。
先交代历史路线之争。阴影体(Shadow Volume)从几何出发:把遮挡物的轮廓挤出侧棱面,用模板缓冲数「片元被几个侧棱面盖住」判断在阴影内外——精确到像素、边界锐利,但几何开销随场景复杂度爆炸、模板操作吃填充率,模板测试那个 2.4 节的关卡被它用到了极限。阴影映射(Shadow Mapping)从图像出发:只问「从光源看过去,这个位置前面有没有别的东西」,一次光源视角的深度渲染解决问题。它对场景复杂度不敏感、天然走 GPU 喜欢的光栅化路线,代价是深度采样的离散化精度问题。实时产业基本一边倒选了阴影映射,本节的解剖对象就是它。
选型的深层理由值得点破:阴影体输在「计算量正比于场景几何复杂度」,阴影映射赢在「计算量正比于屏幕像素数」——像素数是可预算的(分辨率定了就定了),几何复杂度是失控的(美术想加多少细节加多少)。把不可控成本换成可控成本,这类「复杂度转移」的思路在第 6 章(把 CPU 开销转移给 GPU)、第 7 章(把渲染开销转移给剔除)还会反复出现,它是性能工程的第一性原理。

坐标换算是实现里最容易翻车的一步,值得单独拆账。比对用的坐标不是片元的世界坐标,而是「光源 VP 矩阵乘出来的裁剪坐标再透视除法、再映射到 0 到 1 的纹理坐标区间」——因为你要拿它去采样纹理(uv 在 0 到 1)又要比对深度值(深度纹理存的也是 0 到 1 区间)。三段换算在着色器里通常合并成一句:手写位置乘光源 VP 得四分量,除以 w,乘 0.5 加 0.5,xyz 就同时是采样坐标和比对深度。这步换算错任何一段,症状是「阴影整体错位」或「阴影盖满全屏」。
这套换算值得在 GLSL 里落地一次,配合顶点着色器预计算能省下逐片元的矩阵乘法:
// 片元着色器里的比对核心(换算矩阵由顶点着色器预乘传递) uniform sampler2D shadowMap; // 第一遍的深度纹理 in vec4 fragPosLightSpace; // 顶点着色器传来的光源裁剪坐标 float shadowFactor(vec3 normal, vec3 lightDir) { vec3 proj = fragPosLightSpace.xyz / fragPosLightSpace.w; // 透视除法 proj = proj * 0.5 + 0.5; // 映射到 0-1 采样域 float closest = texture(shadowMap, proj.xy).r; // 记录的深度 float current = proj.z; // 自己的深度 float bias = max(0.0008 * (1.0 - dot(normal, lightDir)), 0.0002); // 斜率偏移 return current - bias > closest ? 0.35 : 1.0; // 在阴影里 亮度打折 }
五行代码浓缩了本节全部机制:透视除法、0.5 映射、深度比对、斜率偏移;返回值乘到光照结果上,阴影里的物体不是全黑而是压暗——视觉上更自然也更容易调。
阴影痤疮的根源是深度纹理的分辨率有限:一个纹素覆盖的区域内,「记录的遮挡物深度」是离散采样的,而比对片元的深度是连续的,两者本该相等(同一表面)却总有半个纹素级别的出入。标准药方两味:深度偏移(glPolygonOffset 或着色器里手动加常量,把比对基准推后一点点)与正面剔除重画(画深度通道时剔除朝向光源的面,只画背面——痤疮只长在朝光面上,从背面记录深度天然隔了一个物体厚度)。彼得潘移位是偏移过度的并发症:接触点被推过了头,脚底阴影断开。工程定式是按斜率缩放偏移(GL_TEXTURE_COMPARE 或着色器里按 N 与 L 的夹角加大偏移)——坡面比平面需要更大的容忍。两病的本质是同一根精度绳子的两端,只能在「刺点」与「离地」之间找场景最优,找不到理论最优——这也是级联与过滤技术存在的动机。
硬阴影边界的锯齿来自深度纹理的分辨率上限。PCF(百分比渐近过滤)是软化标配:不只采样一个纹素,而是取邻域 3×3 或 5×5 一圈,按「多少个邻居判它在阴影里」的比例出中间值——阴影边缘出现半影过渡。PCF 的采样半径与偏移量要一起调,半径过大出现「光晕渗漏」(阴影整体变淡)。PCF 的着色器实现有个省事技巧:采样偏移不用写死坐标表,用一组循环生成的旋转泊松盘样本,同样的样本数边界质量更均匀——游戏引擎里阴影质量的最后一档提升多半来自样本分布的改良而不是样本数量的堆砌。
**点光源(灯泡向四面八方)**的光源视角是整个球面,一张深度图装不下——解法是立方体阴影:FBO 挂立方体纹理附件,光源位置为心向六个面各画一遍深度(六次绘制调用,或者几何着色器一次分发六面)。**方向光(太阳)**则用正交投影画深度——平行光没有透视衰减,正交的「w 恒 1」特性(4.3 节)正好匹配。大场景的终极形态是级联阴影贴图:近平面附近用高分辨率深度图、远处用低分辨率,多张深度按距离切换——工程量大但它解决的是「近处影子细腻与远处覆盖范围」这对根本矛盾。级联的接缝处理(两级之间深度值的过渡带混合)是它工程量的主要去处,做不好会在接缝处露出一条阴影错位的缝。
背景:工程师按教程搭好两遍渲染,运行后整个画面被阴影盖死。操作:按管线顺序排查——先确认第一遍深度纹理内容正常(把它当颜色画出来看,一片由白到黑的渐变为正常);再查第二遍的换算链,发现 0.5 加 0.5 的映射被遗漏,采样坐标落在负区间,纹理环绕模式(3.2 节的 REPEAT)把越界坐标卷回图内,比对结果全是「在阴影里」。补上映射后阴影位置正确但满身痤疮,再按正面剔除重画加斜率偏移收尾。解读:满屏阴影查坐标映射、错位查 VP 矩阵、痤疮查偏移与剔除——三类症状对应三个环节,这个排查顺序可以当模板用。变式:如果深度纹理画出来是纯黑或纯白,回头查 FBO 附件挂载与深度格式(3.3 节的完整性检查),那是解剖台二的地盘。
影子落地了,效果栈再加一层:把光照结果的材质从「经验参数」升级成「物理参数」——下一节解剖 PBR,金属度与粗糙度如何接管调参的艺术。