第 29 章 阴影原理:Shadow Map 是怎么”画”出影子的

上一章我们把”远处地面闪”和”边缘狗牙”这两类走样扒透了,也顺带在第 6 章光照的尾巴上(6.4.1)提过一句”阴影贴图 Shadow Mapping”。这一章,我们要专攻”影子”本身——它到底是怎么被计算机”算”出来的。

先问你一个每天见、却从没认真想过的现象:

大晴天你走在路上,脚下拖着一条影子。你往前走,影子跟着往前;你抬手,影子也抬手。影子不是某种”黑色物质”,它只是”光被你挡住、照不到的空缺”。可计算机里没有”光会被挡住”这种物理常识——它只会算数字。那它怎么知道”地面上这个点,该亮还是该暗(在影子里)”?

这问题比听起来难。光是直线传播的,谁挡了谁,现实中自动发生;但计算机里,一个像素”被没被挡”,得有人去判定。这一章,我们就把”判定被挡”这件事,从生活直觉一路扒到引擎里的 Bias、Cascade 等旋钮。

全章钥匙先给你:

阴影的本质,就是”从光源的视角看,这个点是不是被别人挡在了前面”。 而计算机最擅长做的就是”从某个视角拍一张图、记录深度”。所以阴影,说到底就是”再用一张深度图去回答’挡没挡’”。——这正好接上你第 26 章学的”深度图(Z-buffer)“概念:阴影,本质就是一张额外的深度缓冲。

记住这句话,走。

29.1 阴影为什么难:光是直线传播被挡,阴影是”光源看不到的地方”

咱们先回到没有计算机的世界,纯观察。

你屋子里一盏台灯。光从灯泡出发,向四面八方射。它碰到你的手,手后面的墙上就少了一块光——那块少光的,就是影子。你把杯子往灯和墙之间一放,墙上多一个影子。手离灯越近,墙上影子越大越虚;越远,越小越实。

这三件事里藏着阴影的全部秘密:

  1. 光走直线,从光源出发沿确定方向走。
  2. 不透明物体会挡光。光碰到手,手后面那条”光本该经过”的直线,就被截断了。
  3. 影子 = 该有光却没光的地方。它不是”某种黑东西”,是”光的空缺”。

现在换成计算机的思路:我要给地面上某个点 P 上色。我得回答一个问题——“从光源看过去,P 点前面有没有东西挡着?” 如果挡着了,P 就收不到这盏灯的光,该暗(在影子里);如果没挡,P 就被照亮。

这个”挡没挡”的判定,就是阴影算法的全部。难点在于:场景里可能有成百上千个物体,每个都在互相挡;而且你要对每一个被照的点都问一遍这个问题。怎么问得又快又对,就是工程师要解的题。

生活类比:影子像”排队领奖”。光源是发奖台,地面上每个点 P 是个排队的人。如果 P 和发奖台之间被人(物体)挡了视线,P 就领不到奖(没光),成了”阴影里的倒霉蛋”。计算机要做的,就是帮每个 P 查一次”你和发奖台之间有没有人”。

29.2 Shadow Map 的核心思想:从光源视角再渲染一遍,只存”最浅深度”

最主流、你 Unity 里默认用的阴影算法,叫 Shadow Map(阴影贴图)。它的思想极巧,我慢讲。

29.2.1 第一步:从光源的眼,给世界拍一张”深度照”

正常渲染时,你是从”摄像机”的眼看出去的(第 3 部分讲摄像机)。Shadow Map 说:我再虚拟一台相机,架在光源的位置,朝场景拍一张照片——但这张照片不记录颜色,只记录”从光源看,每个方向上最近那个物体,离光源有多远(深度)”。

比如光源是一盏台灯。从台灯看出去,正前方最先碰到的是你的手(离灯 30 厘米),手后面是墙(离灯 200 厘米)。那张”深度照”里,对应”手的方向”的那个像素,记的值是 30;对应”墙的方向”的像素,记的值是 200。

这张”只存深度的图”,就是 Shadow Map(阴影贴图)。它本质就是一张深度图——和你第 26 章学的 Z-buffer 是同一种数据结构,只不过 Z-buffer 是从”摄像机”视角存的深度,Shadow Map 是从”光源”视角存的深度。一个是”眼睛看到的最浅”,一个是”光源看到的最浅”。同宗。

29.2.2 第二步:正常着色时,把每个点也”看向光源”比深度

现在正式渲染画面(从摄像机眼)。轮到给地面上点 P 上色了。我这样判定 P 在不在影子里:

  1. 把 P 也”放”到光源的视角坐标系里,算出”从光源看,P 在这个方向、离光源多远”——记它的深度为 d_P。
  2. 去 Shadow Map 里,找”和 P 同一个光源方向”的那个像素,读出当年存的最浅深度 d_min(也就是”光源视线方向上,第一个挡光的物体离灯多远”)。
  3. 比较:
    • 如果 d_P ≈ d_min(P 就是那个最浅的、离灯最近的):说明从灯看过去,P 前面啥也没有——P 被照亮。
    • 如果 d_P > d_min(P 比最浅的还深):说明在 P 和灯之间,还有一个更近的物体挡着(比如你的手在 P 和灯之间)——那 P 就该在影子里,变暗。

就这么一句比较,影子就”画”出来了。整张 Shadow Map 算法,就是”渲染时多比一次深度”。

连回第 26 章钉死一句话:阴影本质就是一张深度图(从光源视角的 Z-buffer)。 你之前学 Z-buffer 用来”判断哪个物体离摄像机近、该画在前面”(解决遮挡)。Shadow Map 把同一个”深度图”技术,换个视角(光源)再用一次,用来”判断哪个点被挡光”。一次技术,两种用途——这才是理解阴影最省力的钥匙。你能否用”排队领奖”的类比,重新描述一遍 d_P > d_min 这句话?

29.2.3 为什么这招”稳赚”:它只比深度,不真模拟光

你可能会想:真实阴影那么复杂(软边、染色),这么简单比一下深度就够?

答案是:它不求真,求”像”。Shadow Map 不真去算光的物理传播,只是用”深度比较”近似出”挡没挡”——这是第 1 章哲学”用近似换实时”的又一次胜利。它便宜(就多渲染一遍深度 + 每像素比一次),又能动(角色一抬手,重新比一遍就更新影子),所以成了实时渲染的标配。

代价和限制我们 29.3 起细说——因为它”只是比深度”,精度问题会冒出特有的怪毛病。

29.3 shadow acne(阴影痤疮):表面自己”咬”自己

开了 Shadow Map,你满心欢喜一运行——咦?本来该平整受光的墙面上,出现一片片条纹状、像晒伤一样的暗纹,随着视角蠕动。这叫 shadow acne(阴影痤疮)。物体表面在”自己阴影里自己”。

29.3.1 病根:精度不够 + 表面有斜率

为什么表面会”自己挡自己”?回到 29.2.2 的比较。理论上,一个被照亮的平面上的点 P,d_P 应该等于 Shadow Map 里存的 d_min(因为平面自己就是最浅的那个)。但计算机有精度误差:Shadow Map 是一张分辨率有限的图,它把连续的深度量化成了一格一格的值。

问题来了:当表面是斜的(比如一面倾斜的墙、一块斜坡地面),从光源看过去,这面斜面上相邻的屏幕像素,在 Shadow Map 上”对应到的格子”边缘不整齐——有的像素采样到的 d_min 比真实 d_P 略小一点点(量化误差)。于是比较时,d_P 略大于 d_min,引擎误判”前面有东西挡着”→ 把这个点判成”在影子里”→ 暗了一块。

因为误差是随表面斜率、随 Shadow Map 网格周期性变化的,结果就是一条一条的暗纹,趴在表面上蠕动。这就是 acne(痤疮,像皮肤上的痘痕)。

类比:你拿一把不够密的尺子(Shadow Map 精度),去量一面斜放的长板(斜面)。尺子格子粗,斜板在不同格子里的”读数”忽大忽小,你误以为板子”在某些地方凹下去被自己挡了”——其实板子是平的。误差的周期,就是尺子格子的周期,于是显成规律的条纹。

顺带补一个你会关心的细节:为什么真实影子边缘是”虚”的(软),而 Shadow Map 比出来的是”硬”的(一刀两断)?因为 29.2.2 那个比较是”非黑即白”的——d_P 比 d_min 大哪怕一丁点,就整点判黑。但现实里,光源不是理想的点,而是有大小的(比如太阳是个圆盘,不是针尖),于是影子边缘有一段”半挡光”的过渡区,显成柔和虚边。硬比较得不到这段过渡,所以默认 Shadow Map 影子是硬的。要软边,得做”周围多采样几次、取被挡比例”(见 29.7 的 Soft Shadows,底层叫 PCF,Percentage Closer Filtering,百分比渐近滤波——对 Shadow Map 里一个点周围采多个样本比深度,统计”被挡了几成”,换成对应灰度,边缘就虚了)。你先记住”硬/软”的区别来自”比一次 vs 比一片”,具体 PCF 我们 29.7 再落进旋钮。

29.3.2 解法:bias(深度偏移),把比较门槛抬高一点

工程师的解法直接:既然误差让 d_P 偶尔”假大于”d_min,那我比较时,故意把 P 的深度往”靠近光源”方向抬一点点,也就是比较 d_P - bias 和 d_min。

  • 加了 bias 后,原本因误差”假大于”的情况,被这层偏移抵消了——d_P - bias 又小于 d_min,判定为”没挡、被照亮”,痤疮消失。
  • bias 就像给 P 发了一张”优先通行证”:允许它比最浅深度稍远一点,仍算”被照亮”。

这就是 Unity 里 Shadow Bias(阴影偏移) 滑块的来历。它专门对付 acne。

29.4 Peter Panning(彼得潘效应):bias 加太大,物体”浮空”

bias 是个好东西,但加过头就出另一种怪病,图形学幽默地叫它 Peter Panning(彼得潘效应,得名于童话里那个永远不长大的、会飞的小男孩——这里指物体”脚不沾地、飘起来了”)。

29.4.1 病根:bias 太大 → 本该连着的阴影断开,物体像浮空

回忆 bias 是把 P 往光源方向抬。如果 bias 抬得太多,会发生什么?

考虑一个贴地的物体,比如一块地毯、一本书平放在地上。它本该和地面”亲密接触”,接触处阴影是连着的(书底和地面之间不该有缝、不该有光漏进来)。但 bias 把书”抬”得离光源更近了,使得引擎认为”书和地面之间那段,光其实照得到”——于是书底和地面之间出现一条被照亮的细缝,本该连着的接触阴影断开了。视觉上:书像悬浮在空中,脚不沾地。这就是彼得潘——物体”飞”起来了。

29.4.2 bias 的两难:小了 acne,大了浮空

现在你看见了 Shadow Map 最经典的两难:

  • bias 太小 → acne(表面自己咬自己出条纹)。
  • bias 太大 → Peter Panning(物体浮空、接触阴影断开)。

两者此消彼长,像拧一个会打架的旋钮。新手调阴影,一半时间都在和这对冤家搏斗。这也是为什么 Unity 把 Bias 单独拎出来给你调——它不是”越大越好”或”越小越好”,而是要你找到中间那个刚好压住 acne、又没让物体飞起来的值。

反问你:如果你看到”地面上一本书,底下漏出一条光缝、像飘着”,你第一反应该拧 Bias 往哪边?——往小调。反之”墙面一片条纹暗疮”,往大调。先认病,再拧旋钮,这是排查纪律。

29.5 法线偏移 / 斜率缩放 bias:更聪明的 bias,缓解两难

bias 两难太烦人,工程师想了更聪明的办法,不让”一个固定 bias 打天下”。

29.5.1 Normal Bias(法线偏移):沿表面法线往外推

Normal Bias(法线偏移):不是简单地把 P 往光源方向抬,而是沿着 P 所在表面的法线方向,把采样点往外推一点点再去比深度。

为什么更聪明?对于”接近垂直于光线”的斜面(acne 重灾区),沿法线外推,正好把误差敏感性降下来,acne 消了;而对于”贴地物体”,因为它推的方向是”沿表面法线(朝天)“,不会像单纯抬 Bias 那样把”书和地面的接触缝”撑开——所以既压了 acne,又不那么容易浮空。

这就是 Unity 里第二个滑块 Normal Bias 的来历。它和 Shadow Bias 配合:Shadow Bias 管”整体深度偏移”,Normal Bias 管”沿法线智能偏移”。两者一起调,比单拧一个 Bias 稳得多。

29.5.2 Slope Scale Bias(斜率缩放偏移):按斜率加大

更进一步的做法叫 Slope-Scaled Depth Bias:bias 的大小随表面相对光线的斜率自动变化——越陡的斜面(acne 越易发),给越大的 bias;越平的面,给越小的 bias。这样”需要大 bias 的地方才大,不需要的地方不大”,从根上缓和了两难。

你不用记具体公式,带走的就一句:现代引擎的 bias 不是常数,而是”按表面朝向智能变化”的。 Unity 的 Shadow Bias + Normal Bias 组合,背后就是这个思想。理解它,你就不会再把 Bias 当”随便拧拧”的旋钮,而是知道它在和”精度误差 vs 浮空”两只鬼打架。

我给你一个实战口诀,省得你第一次面对 Bias 滑块发懵:先把 Normal Bias 设到一个中等正值(它最不容易惹浮空),再小幅加 Shadow Bias 压 acne,边看画面边拧。如果 acne 压不下去又不敢再加 Bias,回头去抬高 Shadow Map 的 Resolution——精度上来了,误差本身变小,acne 自然减轻,且不会带来浮空。所以”acne 的三味药”是:Normal Bias、Shadow Bias、Resolution,三管齐下比单拧一个 Bias 稳得多。你调的时候,记住目标是”条纹消失、且物体脚还踩在地上”,两个条件同时满足才算调好。

29.6 级联阴影 CSM:大场景里远处阴影糊,因为一张图覆盖太大

你满以为调好 Bias 就天下太平,结果跑进一个大开放世界——近处影子还行,可远处山的影子、几百米外建筑的影子,糊成一团、边缘像马赛克、甚至干脆消失。这是 Shadow Map 的又一个结构性毛病。

29.6.1 病根:一张 Shadow Map 要覆盖整个视锥,精度被摊薄

回忆第 7 章(视锥)和第 8 章(远裁剪):摄像机看到的范围,是一个从近到远越来越宽的”金字塔”(视锥)。Shadow Map 是”从光源视角拍的一张深度图”。如果你用一张 Shadow Map 去覆盖”从你鼻子尖到远裁剪面”的整个范围——

想象一下:这张图总共就 2048×2048 个格。它要代表”近处几厘米”到”远处几公里”的所有深度。那近处一寸地面,分到的格子极少;远处的山,分到的也极少。精度被整个大范围摊薄了,于是近处阴影边缘锯齿、远处阴影直接糊掉。

这和你在第 3 章学的”分辨率/PPI”是同一回事:总格子数固定,铺的物理范围越大,密度越低,越糙。阴影图也不例外。

29.6.2 解法:CSM(Cascaded Shadow Maps,级联阴影贴图)

工程师的解法优雅,思想你一定秒懂——把那一大段距离,切成几段(近、中、远),每一段单独用一张 Shadow Map。

  • 近段(你脚边 0~20 米):一张图只管这 20 米,精度极高,近处影子锐利。
  • 中段(20~80 米):另一张图管,精度够用。
  • 远段(80~500 米):再一张,虽然相对稀,但远处理本就糊(呼应第 8 章雾化”远处该糊”),你也不在乎它多锐。

这几张图像”级联”的台阶,所以叫 Cascade(级联)。合成时,每个点按自己离镜头的距离,去用对应的那张图比深度。

生活类比:你给一幅超长画卷拍照,想全清楚,一张照下来远处必糊。不如分段拍——近景特写一张、中景一张、远景一张,最后拼起来,每段都清楚。CSM 就是”阴影图的分段特写”。它和第 8 章”远裁剪面把世界切成近远”、第 7 章”视锥分近远平面”,是同一个”按距离分层”的思路。

代价:多几张 Shadow Map,多几次”从光源渲染深度”的开销。但相比”一张糊全局”,这开销换来远处可用的阴影,太值。Unity 里 Light 组件的 Cascade Count(级联数,常 2~4) 就是调这个——级联越多,远处越清,但越贵。

29.7 Unity 实操:Shadow Type、Resolution、Bias、Cascade 怎么拧

讲完原理,落到你最关心的——Unity 里一个 Light 组件,阴影相关的旋钮各对付什么。我按”它解决什么问题”逐个给。

29.7.1 Light 组件的 Shadow 区块

  • Shadow Type(阴影类型):
    • No Shadows:不要影子,最便宜。适合性能极紧或纯风格化(比如卡通故意无影)。
    • Hard Shadows:硬边影子,边缘锐利如刀切,便宜。像正午太阳高照的影子。
    • Soft Shadows:软边影子,边缘虚化柔和,真实但贵(要多采样做模糊)。像阴天/晨昏的影子。绝大多数写实项目选它。它底层就是 29.3 末尾提的 PCF——对 Shadow Map 里一个点周围采多个样本比深度、统计被挡比例换灰度,于是硬边变虚边;采样越多越柔越贵。
  • Resolution(阴影图分辨率):这张 Shadow Map 做多大(如 512/1024/2048/4096)。低了影子边缘锯齿、像马赛克;高了清晰但吃显存和带宽。这是”阴影清晰度 vs 算力”的直接权衡,和你在第 3 章学的渲染分辨率同理。
  • Bias(阴影偏移) 与 Normal Bias(法线偏移):就是 29.3~29.5 那对冤家。前者整体深度偏移,后者沿法线智能偏移。一起调,找 acne 和浮空之间的平衡点。
  • Cascade Count(级联数,方向光特有):29.6 的 CSM。2~4 级,级联越多远处越清越贵。

29.7.2 一张”对症下旋钮”的排查表

我把这一章所有怪现象整理成表,你眼睛看到啥,对号入座:

现象真病根拧哪个
墙面一片条纹暗疮、随视角蠕shadow acne(bias 太小)调大 Shadow Bias / 加 Normal Bias
物体脚不沾地、底下漏光缝(飘了)Peter Panning(bias 太大)调小 Shadow Bias / 减 Normal Bias
近处影子边缘锯齿、马赛克Shadow Map 分辨率低调高 Resolution
远处山/建筑影子糊掉、消失一张图覆盖太广、精度摊薄加 Cascade Count(如 2→4)
影子整体太硬、像剪纸用了 Hard Shadows切 Soft Shadows
完全没影子Shadow Type = No切 Hard/Soft
性能被阴影吃垮图太大/级联太多/软阴影降 Resolution、减 Cascade、或暂用 Hard

排查纪律再强调一遍:先认病(acne 还是浮空?近处糊还是远处糊?),再拧对应旋钮。新手最容易”影子有问题就无脑拉高 Resolution”——可如果是 acne,拉分辨率没用,得调 Bias;如果是远处糊,才该加 Cascade。认错病,白拧。

29.8 收个尾,连回主线与前面章节

咱们把这一章扣回全书的哲学:

  • 自然现象:光走直线,被不透明物体挡 → 挡住处成影(光的空缺)。
  • 现实问题(工程师的麻烦):计算机不会”自动挡光”,得对每个被照点判定”挡没挡”;且要实时、要能动。
  • 工程师方案 → 落进引擎的旋钮:
    • Shadow Map = 从光源视角再渲一遍、只存最浅深度的一张深度图;着色时把点也看向光源比深度,d_P > d_min 即在影中。它本质就是第 26 章学的深度图(Z-buffer),只是换了光源视角。
    • shadow acne → 精度/斜面误差,解法 Bias(深度偏移)。
    • Peter Panning → Bias 太大,物体浮空;引出 bias 两难。
    • Normal Bias / 斜率缩放 → 更聪明的、按表面朝向变化的 bias,缓解两难。
    • CSM 级联阴影 → 大场景一张图覆盖太广精度摊薄,分近中远几段各用一张;连回第 7 章视锥、第 8 章远裁剪的”按距离分层”。
    • Unity 旋钮:Shadow Type / Resolution / Bias / Normal Bias / Cascade Count。
  • 你能做的:看到条纹痤疮调大 Bias,看到浮空调小 Bias,看到远处糊加 Cascade,看到锯齿调高 Resolution。每味药对应一个病。

再抬头看那张贯穿全书的表——这一章又是”现实连续 → 计算机离散(深度图有精度极限)→ 出假象(acne/浮空)→ 工程师用预处理+参数去补”的亲兄弟。而且你发现没:第 26 章的 Z-buffer、本章的 Shadow Map,共用”深度图”这一招;第 3 章的分辨率、第 8 章的远裁剪、本章的 CSM,共用”按距离分层求精度”这一招。全教程的概念,像一张网一样越收越紧。

到此,图形学深度的两块硬骨头(走样、阴影)你都啃下来了。后面若讲”全局光照""后处理”,你会发现它们还是这张网的延伸。

【思考题】

  1. 为什么说”阴影本质是一张深度图”?它和你第 26 章学的 Z-buffer 是什么关系?用”从光源视角再渲一遍、只存最浅深度”这句话,解释 Shadow Map 怎么判定一个点”在不在影子里”。
  2. 什么是 shadow acne(阴影痤疮)?它的病根是精度误差还是光照算错?工程师用什么旋钮治它?这个旋钮如果加得太大,会引出什么新病?
  3. 解释”bias 两难”:为什么说 Shadow Bias”小了 acne、大了浮空”?Normal Bias(法线偏移)相比单纯 Shadow Bias,聪明在哪,为什么能缓解两难?
  4. 大开放世界里,近处影子还行、远处山的影子却糊成一团甚至消失。这是什么结构性毛病?CSM(级联阴影)怎么治它?它和第 7 章视锥、第 8 章远裁剪的”按距离分层”思想有什么联系?

【参考答案】

  1. 阴影 = “从光源看,这个点是否被别人挡在前面”。计算机最擅长的就是”从某视角拍图、存深度”,所以阴影就实现成”从光源视角渲一遍、只存每个方向最浅深度”的一张深度图(Shadow Map)。着色某点 P 时,把 P 也看向光源算深度 d_P,去图里找同方向存的最浅深度 d_min:若 d_P ≈ d_min,P 就是最浅的、没被挡→照亮;若 d_P > d_min,说明 P 和光源之间还有更近的物体→P 在影中。它和第 26 章 Z-buffer 是同一种数据结构:Z-buffer 是从摄像机视角存的深度(解决遮挡谁在前),Shadow Map 是从光源视角存的深度(解决谁挡光)。一次”深度图”技术,换了视角用两次。
  2. acne 是”表面自己咬自己出的条纹暗疮”,病根不是光照算错,是 Shadow Map 精度有限 + 斜面量化误差:斜面上相邻像素在阴影图里对应格子边缘不齐,采样到的 d_min 偶尔比真实 d_P 略小,导致误判”被挡”而变暗,误差周期显成条纹。治法:加 Shadow Bias(深度偏移),把 P 往光源方向抬一点,抵消误差。但 Bias 加太大→引出 Peter Panning(彼得潘效应):贴地物体被抬得”脚不沾地”,本该连着的接触阴影断开、漏出光缝,像浮空。
  3. bias 两难:小了压不住精度误差→acne;大了把物体抬太高→浮空断影。两害此消彼长。Normal Bias 聪明在:不是整体往光源抬,而是沿表面法线方向外推一点再去比深度。对 acne 重灾区的陡斜面,沿法线外推正好降敏、消 acne;对贴地物体,推的方向是”朝天(沿法线)“,不会像单纯 Shadow Bias 那样把”书与地面的接触缝”撑开→所以既压 acne 又不那么易浮空。它把”固定 bias”升级成”按表面朝向智能偏移”,从根上缓和两难。
  4. 这是”一张 Shadow Map 覆盖整个视锥、精度被大范围摊薄”的结构病:总格子数固定,铺的物理范围越大(近到远全包),密度越低越糙,远处山影直接糊掉。CSM 治法是把距离切成近/中/远几段,每段单独一张 Shadow Map,近段精度极高、远段相对稀(远处理本就糊,呼应第 8 章雾化),合成时按点距镜头远近选用对应图。它和第 7 章视锥(分近/远平面)、第 8 章远裁剪(按距离切世界近远)同一思路:都是”按距离分层,把有限的精度/算力优先给重要的近处”。Unity 里 Cascade Count 就是调级数,越多远处越清越贵。你能否想清楚:为什么”远处阴影略糊”玩家通常不介意,而”近处阴影糊”完全不能忍?(提示:连回第 8 章”远处本就该糊”的感知弱点。)